Skip to content
ecdsa.com

HS256 vs RS256: shared-secret or public-key JWTs

HS256 is an HMAC: every verifier can also issue tokens. RS256 is a signature: verifiers hold only a public key. Most JWT design mistakes start with not noticing the difference.
ComparedHS256RS256

HS256 vs RS256 is the first decision when you issue JWTs. The two alg values look similar, but HS256 authenticates tokens with a shared secret (HMAC-SHA256) while RS256 signs them with an RSA private key (RSASSA-PKCS1-v1_5 with SHA-256). That changes who can create tokens, how keys are distributed and what happens when a key leaks.

At a glance

HS256RS256
AlgorithmHMAC with SHA-256 (RFC 7518 §3.2)RSASSA-PKCS1-v1_5 with SHA-256 (§3.3)
KeyOne shared secret, at least 256 bitsRSA key pair, at least 2048 bits
Who can verifyHolders of the secretAnyone with the public key
Who can mint tokensEvery holder of the secret, including verifiersOnly the private key holder
Key distributionConfidential copy to every verifierPublic JWKS (jwks_uri)
Signature part32 bytes, 43 base64url characters256 bytes, 342 characters (RSA-2048)
Example token (measured below)187 characters508 characters
Speed, one Apple M4 Max coremillions of HMACs per secondabout 3,000 signatures, 114,000 verifications per second
JWA requirement levelRequiredRecommended
Main failure modeWeak or leaked secret: anyone can mint tokensAlgorithm confusion in lax verifiers; key handling

The trust model

With HS256 the verification key and the signing key are the same bytes. Every service that verifies tokens can also create valid tokens for any user, scope or audience, and a secret leaked from any one of them (an environment variable in a log, a front-end bundle, a mobile app) lets anyone mint tokens. That is acceptable for a monolith that issues and checks its own session tokens; it is a serious weakness when a token issuer serves many APIs or teams.

With RS256 the issuer keeps the private key, ideally in an HSM or KMS, and publishes the public key in a JWKS at a URL listed in its metadata. Verifiers fetch the key set, select keys by kid and can be added or removed without sharing secrets. A compromised verifier can read tokens but cannot forge them. This is why OpenID Connect providers sign ID tokens with RS256 by default and why the JWT best-practices RFC (RFC 8725) steers multi-party deployments to asymmetric algorithms.

OpenID Connect does allow HS256 ID tokens for confidential clients, keyed with the client secret; that works because each client verifies only tokens issued to itself.

Secret strength and offline cracking

An HS256 token is a perfect offline test oracle: anyone who sees one token can try candidate secrets as fast as their hardware computes HMAC-SHA256, without talking to your server. Short, human-chosen or default secrets ("secret", a project name, the example from a tutorial) are cracked in seconds with common password-cracking tools. RFC 7518 requires a key at least as long as the hash output, 256 bits for HS256, and RFC 8725 §3.5 says keys must have enough entropy. Generate the secret with a CSPRNG (openssl rand -base64 32) and never derive it from a password.

RS256 has no equivalent offline attack on the private key, but RSA keys below 2048 bits are not allowed by RFC 7518 and the key file must be protected like any private key.

Algorithm confusion between them

The best-known JWT vulnerability mixes the two. A service verifies RS256 tokens by calling a library with the RSA public key in PEM form. If the library takes the algorithm from the token header and also supports HS256, an attacker sends a token with "alg": "HS256" whose HMAC was computed with the public key's PEM bytes as the secret; the library uses the same bytes and the forgery verifies. Tim McLean described this in 2015 together with "alg": "none", and RFC 8725 §2.1 and §3.1 codify the fix: the verifier, not the token, decides which algorithm each key may be used with.

Modern libraries make this hard: jsonwebtoken refuses to use a public key as an HMAC secret (the error secretOrPublicKey must be an asymmetric key appears when configurations mix them up), PyJWT requires an algorithms argument, and jose binds algorithms to key types. Still, always pass an explicit allowlist with exactly the algorithm you expect.

Size and speed

The script below, run with Node.js 26.10, builds the same claims as HS256 and RS256 tokens with node:crypto only.

Node.js: HS256 and RS256 tokens for the same payload
import { createHmac, generateKeyPairSync, randomBytes, sign } from "node:crypto";

const b64url = (value) => Buffer.from(value).toString("base64url");
const payload = b64url(JSON.stringify({ iss: "https://auth.example.com", sub: "user-42", aud: "api", exp: 1792500000 }));

// HS256: HMAC-SHA256 with a shared 256-bit secret.
const secret = randomBytes(32);
const hsInput = `${b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }))}.${payload}`;
const hs256 = `${hsInput}.${b64url(createHmac("sha256", secret).update(hsInput).digest())}`;

// RS256: RSASSA-PKCS1-v1_5 with SHA-256 and a 2048-bit key.
const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
const rsInput = `${b64url(JSON.stringify({ alg: "RS256", typ: "JWT", kid: "2026-10" }))}.${payload}`;
const rs256 = `${rsInput}.${b64url(sign("sha256", Buffer.from(rsInput), privateKey))}`;

console.log("HS256 token:", hs256.length, "characters, signature part:", hs256.split(".")[2].length);
console.log("RS256 token:", rs256.length, "characters, signature part:", rs256.split(".")[2].length);
Output
HS256 token: 187 characters, signature part: 43
RS256 token: 508 characters, signature part: 342

The RS256 header also carries a kid, which verifiers need to pick the key from a JWKS. In production use a maintained JWT library rather than hand-built tokens; the point here is the size of the signature part. For an ECDSA comparison of the same trade-off, see ES256 vs RS256.

When to choose which

  • A single application issuing and checking its own session tokens: HS256 with a 32-byte random secret, rotated with a short overlap.
  • An authorization server whose tokens are checked by several APIs: RS256, PS256 or ES256 with keys published in a JWKS.
  • Tokens verified by third parties, mobile apps or browsers: asymmetric only; a secret shipped to a client is public.
  • OpenID Connect ID tokens for confidential clients: either works; asymmetric is the default and simpler to operate.
  • Webhooks to individual customers: HMAC-style schemes are common because each customer has its own secret.
  • Compliance-driven key custody (HSM, KMS): asymmetric keys, since services can verify without access to signing material.

Migration notes

Moving from HS256 to RS256 or ES256 without a flag day:

  1. Create the key pair in a KMS or HSM and publish the public key in a JWKS with a new kid and alg.
  2. Teach verifiers both: select the key by kid, and bind each key to one algorithm (the shared secret to HS256, the new key to RS256). Never let one verification call accept both algorithms for the same key material.
  3. Start issuing asymmetric tokens with the new kid; existing HS256 tokens keep verifying until they expire.
  4. Remove the shared secret from every verifier once the longest HS256 token lifetime has passed. This step is the actual security gain.
  5. Rotate the old secret anyway if there is any chance it leaked, since an attacker holding it could still mint HS256 tokens until verifiers stop accepting them.

References

All comparisons