StringToolsStringTools
Back to Blog
SecuritySeptember 8, 2026·9 min read·Mitul Mandanka

HS256 vs RS256: Choosing a JWT Signing Algorithm

By Mitul Mandanka·Reviewed for accuracy·Last updated September 8, 2026

HS256 or RS256? The Answer Is About Who Verifies

HS256 is symmetric: one shared secret both signs and verifies, so every party that can check a token can also forge one. RS256 is asymmetric: a private key signs and a public key verifies, so verifiers cannot mint tokens. Choose HS256 inside a single trust boundary, RS256 whenever anyone else verifies.

Key Takeaways

  • The alg header names the algorithm, but your verifier must never take instructions from it. Pin the expected algorithm in code and reject anything else.
  • *HS\ is not weaker cryptography.** HMAC-SHA-256 with a 256-bit random secret is excellent. What is weak is the operational model once that secret must be copied to a second party.
  • Asymmetric signing is what makes JWKS possible. Publishing a public key is safe; publishing an HMAC secret is handing out your signing key.
  • Algorithm confusion turns a published RSA public key into an HMAC secret the attacker already has. It is a bug in how you call the verifier, not in RSA.
  • kid is an untrusted lookup hint. Use it as an opaque map key, never as a filename, SQL fragment or URL.
  • Rotation needs two overlapping windows: one for JWKS cache expiry, one for outstanding token lifetime.

This post assumes you know a token's three-part structure; if not, start with how to decode a JWT. What follows covers only the alg header and the keys behind it.

Symmetric Signing: One Secret With Two Powers

With HS256, HS384 and HS512 the signature is an HMAC over base64url(header) + "." + base64url(payload) using a shared secret. Verification is not a different operation from signing: the verifier recomputes the same HMAC and compares. One consequence dominates every other design decision.

The capability to verify and the capability to forge are the same capability. There is no way to give a service the ability to check tokens without also giving it the ability to mint tokens for any user, any role, any expiry.

Inside one trust boundary that is fine, and often right. A monolith that issues its own session tokens and verifies them on the next request is signing notes to itself: HMAC is fast, the signature is small, and there is no key to distribute. It stops being fine the moment the secret travels.

  • A gateway and three downstream services all verify tokens, so the secret now sits in four deployment configs.
  • A partner needs to validate tokens you issue, so you send them the secret. They can now issue tokens as you.
  • Someone embeds it in a mobile app or SPA so the client can "check" the token. Binaries and JavaScript bundles are readable, so anyone can now forge an admin token.

There is also a brute-force angle specific to JWTs. A token is signed plaintext: the attacker holds the exact message and the exact tag, so offline guessing parallelises trivially, and password-cracking tools ship dedicated JWT modes. A secret like supersecret falls in seconds. RFC 7518 requires a key at least as long as the hash output, so 256 bits for HS256, and it must come from a CSPRNG rather than a passphrase. If you want to see what a value of that width looks like in hex, the hash generator produces digests of exactly 256 bits.

Use HS\* when one system owns both ends. Treat any request to share the secret as a signal you have outgrown it.

Asymmetric Signing: Splitting Verify From Mint

RS256, PS256, ES256 and EdDSA use a key pair. The issuer holds the private key and signs; everyone else holds only the public key and verifies. The public key is not a secret, and a verifier holding it cannot produce a valid signature. That asymmetry is why modern identity infrastructure works the way it does.

  • JWKS distribution. The issuer publishes its public keys as a JSON Web Key Set, conventionally at /.well-known/jwks.json and advertised as jwks_uri in the provider's discovery document. Verifiers fetch and cache it, needing no shared configuration beyond a URL.
  • Third-party verification. A partner, a customer or an edge proxy can validate your tokens without gaining the ability to issue any.
  • Blast radius. A compromised verifier leaks nothing that lets an attacker sign, which is why the private key belongs in an HSM or KMS that signs on request and never exports it.

The four families differ in practical ways rather than in strength.

  • RS256 is RSA with PKCS#1 v1.5 padding, and it is the interoperability default: if something consumes JWTs at all, it supports RS256. Signatures are large — 256 bytes for a 2048-bit key, or 342 base64url characters on every token.
  • PS256 uses the same RSA keys with PSS padding, which is randomised and has a cleaner security argument. Signature size is identical; support is good but not universal.
  • ES256 is ECDSA on P-256. Signatures are 64 bytes, keys are tiny, and signing is far faster than RSA, though verification is slower than RSA's unusually cheap verify. ECDSA is unforgiving of implementation errors: a repeated nonce leaks the private key, and the 2022 Java flaw known as Psychic Signatures (CVE-2022-21449) made verification accept an all-zero signature. Use the platform's vetted implementation and keep it patched.
  • EdDSA in JOSE means Ed25519. Signing is deterministic, so there is no nonce to reuse, signatures are 64 bytes, and it is fast at both ends. It is the best default when you control every verifier and the worst when you do not, because older stacks predate it.

Across all four, one rule holds: a client never holds a signing key.

The Algorithm Table

Sizes below are the raw signature, then the base64url length that actually lands in the token. RSA figures assume a 2048-bit key, the minimum RFC 7518 permits.

AlgorithmTypeKey needed to verifySignature bytesbase64url charsPick it when
HS256Symmetric, HMAC-SHA-256The same secret that signed3243One service signs and verifies, and the secret never leaves it
HS384Symmetric, HMAC-SHA-384The same secret that signed4864A policy mandates a 384-bit MAC
HS512Symmetric, HMAC-SHA-512The same secret that signed6486As above; rarely a practical gain over HS256
RS256Asymmetric, RSA PKCS#1 v1.5Public key256342Maximum interoperability; unknown or third-party verifiers
PS256Asymmetric, RSA-PSSPublic key256342New RSA deployments where every verifier supports PSS
ES256Asymmetric, ECDSA P-256Public key6486Token size or signing throughput matters
EdDSAAsymmetric, Ed25519Public key6486You control every verifier and want a modern, misuse-resistant scheme

The practical reading: HS\* is small and simple and costs you the trust boundary. Among the asymmetric options, RS256 buys compatibility at roughly 260 extra characters per token, while ES256 and EdDSA buy compact tokens at the cost of narrower support. Where proxies cap request headers around 8 KB, that difference is worth measuring rather than guessing.

Algorithm Confusion: When a Public Key Becomes an HMAC Secret

This is the attack that makes people nervous about JWTs, and it deserves stating exactly.

Suppose a service issues tokens with RS256 and verifies them with the matching RSA public key. The verification call is written so the library reads alg from the header and dispatches on it, and the key it was handed is a string or byte buffer rather than a typed key object. Then:

1. The attacker obtains the public key. This is not an obstacle: it is served from the JWKS endpoint or sits in a certificate, because publishing it is the point. 2. The attacker writes a header of {"alg":"HS256","typ":"JWT"} and any payload they like, say {"sub":"1","role":"admin"}. 3. The attacker computes HMAC-SHA-256(base64url(header) + "." + base64url(payload), K), where K is the exact byte sequence of the public key as the server holds it — typically the PEM text including its header line and trailing newline. 4. The server sees alg: HS256, selects HMAC, and uses the key material it has, that same PEM, as the shared secret. The tags match. The token verifies.

The attacker has forged a token from public information alone. Note step three: the forgery must use the server's byte-for-byte representation of the key. PEM versus DER, PKCS#1 versus SPKI, trailing newline or not, all give different HMAC secrets. In practice an attacker enumerates a handful of encodings, which is a small search space, not a defence.

The degenerate case is alg: none. The JWS specification defines an unsecured token, written header.payload. with an empty third field, for contexts where integrity comes from elsewhere. A verifier that dispatches on the header will conclude that a token with no signature has a valid signature. In the 2015 round of JWT library disclosures both classes were found across many popular libraries at once, and some string comparisons were bypassable with None or nOnE.

The defence is one idea applied twice. The algorithm is a property of your configuration and your key, not of the token.

  • Pin it at the call site: jwt.verify(token, key, { algorithms: ['RS256'] }) in Node, jwt.decode(token, key, algorithms=["RS256"]) in PyJWT. An allow-list of one, wherever your language puts it.
  • Better, derive it from the key. A JWK carries alg and key_ops; use those, and treat the header's alg as something to reject a mismatch against, never to select behaviour.
  • Keep HMAC secrets and asymmetric keys in separate stores, so no lookup can return one where the other is expected.
  • Prefer libraries that take typed key objects rather than raw strings, which makes the confusion structurally impossible instead of merely disallowed.
  • Never accept none on a token you authenticate with. There is no configuration in which that is right for a session or access token.

RFC 8725, the JWT best current practices document, makes the same point in normative language and is worth reading before you ship a verifier.

kid, jku and the Rest of the Untrusted Header

Once you accept that the header is attacker-controlled, the other header parameters deserve the same scepticism.

kid is defined in RFC 7515 as a hint indicating which key was used. A hint, carried in the token, chosen by whoever sent it. It exists so a verifier holding several keys picks the right one first time instead of trying each in turn. It carries no authority.

That makes kid a classic injection sink, because implementations naturally turn it into a lookup.

  • Path traversal. A verifier that reads its key from keys/{kid}.pem can be pointed at any file on disk. The prize is a file whose contents the attacker knows or controls: an empty file, a predictable static asset, or /dev/null, yielding an HMAC secret of zero bytes they can then sign with.
  • SQL injection. A verifier that selects the key with a concatenated query can be made to return an attacker-supplied value as the key.
  • Command injection, wherever kid reaches a shell.

The rule is simple: **kid is an opaque key into a fixed map.** Look it up in an in-memory dictionary or a parameterised query, validate it against a conservative character set, and reject any token whose kid you do not already know.

The same reasoning condemns jku and x5u: they tell the verifier where to fetch keys from, so an attacker can serve their own key set and sign with the matching private key. Unless you have a specific need and a strict URL allow-list, disable them.

When triaging a token by hand, the JWT decoder shows the header including alg and kid and flags alg: none, in the browser. It deliberately does not verify signatures, because verification needs a key and a decoder has no business asking for one. Read what a token claims about itself there, then verify properly in code.

Rotating Keys Without Locking Anyone Out

Rotation is not simply generating a new key. It is a scheduled overlap, and the schedule is set by two independent timers: how long verifiers cache your JWKS, and how long already-issued tokens stay valid.

StepActionWait before the next step
1Generate the new key pair. Publish the new public key in JWKS beside the old one, under a new kid. Keep signing with the old key.The longest JWKS cache TTL any verifier uses, plus a margin
2Switch the signer to the new private key. New tokens carry the new kid; old tokens still verify against the still-published old key.The longest lifetime of any token signed with the old key
3Remove the old public key from JWKS and destroy the old private key.Done

Skipping step 1's wait produces the classic rotation outage: tokens arrive with a kid verifiers have not fetched yet. Skipping step 2's wait kills live sessions.

  • **Cache JWKS and refresh on an unknown kid, but rate-limit that refresh.** Without a limiter, anyone can send tokens with random kid values and turn your verifiers into a load generator aimed at your own identity provider.
  • Never trial-verify against every key you hold as a substitute for kid. It works until one of those keys is a stale one you meant to retire.
  • Rotating a symmetric secret is harder. Every holder needs the new value over a secure channel, and through the overlap each verifier accepts two secrets while one is used for signing. The hand-offs grow with the number of holders, which is another way of saying HS\* does not scale past one trust boundary.
  • Have a break-glass rotation. Scheduled rotation is hygiene; compromise rotation means pulling a key now and accepting that live tokens die. Know which you are doing and where the switch is. That inability to invalidate one session cheaply is a genuine trade-off against server-side state, covered in JWT vs session cookies.

A Checklist You Can Diff Against Your Own Code

Take this to the verification path in your codebase, not to a design document. Each line is something you can grep for in ten minutes.

  • The verify call passes an explicit algorithm allow-list with one entry. If a framework hides the call, find where it configures the algorithm and confirm it is not empty, wildcarded, or read from the token.
  • **none appears in no allow-list**, and no code path skips verification when the signature segment is empty.
  • HMAC secrets are 32 or more random bytes from a CSPRNG, live in a secret manager, and appear in no repository, image layer or client bundle. Any secret ever committed is public: rotate it.
  • Private signing keys are non-exportable where the platform supports it, so signing happens in the KMS or HSM rather than in application memory.
  • **kid is resolved through a map or a parameterised query**, validated against an allow-list, and never becomes a path or URL. jku and x5u are disabled.
  • JWKS fetching has a timeout, a cache and a rate-limited refresh, and fails closed.
  • Issuer and audience are checked on every token, so a valid token minted for a different service is rejected by yours. A good signature is not authorisation, a theme that runs through API security best practices.
  • Expiry is enforced with a bounded clock skew, typically 60 seconds.
  • **A test feeds your verifier a token with alg swapped to HS256, signed with your own public key**, and asserts rejection. Without that test you do not actually know the answer.

The short version: the token tells you what it wants to be checked against, and your job is to ignore it. Decide the algorithm and the key from your own configuration, treat every header field as input from a stranger, and keep the rotation overlap wide enough that rotation is boring.

Frequently Asked Questions

Is HS256 less secure than RS256?

Not as cryptography. HMAC-SHA-256 with a 256-bit random secret has no practical break and is faster than RSA. The difference is operational: with HS256 the verify key and the sign key are the same value, so every system that checks tokens can also issue them, and every copy is another chance to leak. RS256 removes that, because the public key can be published without risk. If exactly one service signs and verifies, HS256 is reasonable. As soon as a second party verifies, asymmetric signing is the correct choice.

What does `alg: none` mean, and is it ever legitimate?

It is the JWS unsecured token: a token with an empty signature segment, intended for cases where integrity is guaranteed by another layer, such as a JWS nested inside an already-authenticated envelope. For an access or session token it is never legitimate. A verifier that trusts the header will accept an unsigned token as valid, which is a complete authentication bypass. Keep none out of every allow-list and reject it explicitly rather than relying on a library default.

Should I move from RS256 to PS256 or EdDSA?

Only if every verifier supports it, and treat the move as a key rotation with an overlap window, not a config flip. PS256 uses the same RSA keys with better padding, so it is a modest, low-risk upgrade where support exists. EdDSA gives 64-byte signatures and a deterministic scheme with fewer ways to misuse it, which is attractive when you own every consumer. If any third party or older library validates your tokens, RS256 remains the safe interoperability choice and is not a weak option.

How do I tell which algorithm a token was signed with?

The alg field in the header, which is base64url and readable without any key. Paste the token into the JWT decoder to see the header, the kid and the claims. Treat what you read as a claim by the sender rather than a fact: an attacker can put anything in that field, which is exactly why your server must pin the expected algorithm instead of reading it from the token.

How often should JWT signing keys be rotated?

There is no single correct interval, and the more useful question is whether you can rotate at all without an outage. Many teams rotate on a monthly or quarterly cadence. What matters more is that the process is automated, that JWKS carries old and new keys through an overlap, and that you have rehearsed an emergency rotation. If rotating a key needs a coordinated release across several teams, shorten that path first and worry about the interval second.

Can I verify a JWT in the browser or in a mobile app?

You can decode it there to read claims for display, and with a public key you can technically verify an asymmetric signature. It buys nothing security-wise, because a client can be modified to skip the check. The verification that matters happens on the server that acts on the token. Never ship an HMAC secret or a private key in a client bundle or app binary; both are extractable, and either one lets an attacker mint tokens.

Sources and references

RFC 7518 (datatracker.ietf.org) · RFC 8725 (datatracker.ietf.org) · RFC 7515 (datatracker.ietf.org). Content was reviewed against these sources as of the last-updated date above; external figures and rules may change after publication.