All posts

HS256 vs RS256 for beginners

HS256 vs RS256 explained: shared secret vs key pair, who holds which key, how JWKS and kid work, HMAC key length rules, and when to choose each algorithm.

By Last reviewed 5 min readJWTSecurityCryptography

HS256 and RS256 tokens look almost identical: the same three segments, the same claims, a different alg in the header and a longer signature on the RS256 one. The real difference is who can create tokens and who can only check them, and that decides which one your system should use.

You can sign test tokens with HS256 in the JWT Builder and inspect any RS256 token in the JWT Decoder, which names the algorithm from the header. This guide explains what sits behind each name.

HS256: one shared secret

HS256 is HMAC with SHA-256 (RFC 7518, section 3.2). The same secret signs and verifies: the verifier recomputes the HMAC over header.payload and compares it with the signature. The signature is 32 bytes, which is 43 Base64URL characters.

The catch is symmetry. Anything that can verify an HS256 token can also mint one. With one service that issues and checks its own tokens, that is fine. With five services sharing the secret, each one is a place it can leak, and any of them can forge tokens the other four will accept.

RS256: a private key and a public key

RS256 is RSASSA-PKCS1-v1_5 with SHA-256 (RFC 7518, section 3.3). The issuer signs with an RSA private key; verifiers check with the matching public key, which can't produce signatures. RFC 7518 requires RSA keys of at least 2048 bits for these algorithms, and a 2048-bit key gives a 256-byte signature: 342 Base64URL characters.

Who holds what:

  • HS256: the issuer holds the secret, and every verifier holds the same secret, so every verifier could also issue tokens.
  • RS256: only the issuer holds the private key. Verifiers hold the public key, and so can anyone else, because publishing it is safe.

Same claims, two signatures

Node's built-in crypto module shows the difference in a few lines:

const crypto = require("node:crypto");
const b64url = (s) => Buffer.from(s).toString("base64url");
const payload = b64url(JSON.stringify({ sub: "user_8f3a2c", exp: 1792490400 }));

// HS256: one secret signs and verifies
const secret = crypto.randomBytes(32); // 256 bits
const hsInput = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" })) + "." + payload;
const hsSig = crypto.createHmac("sha256", secret).update(hsInput).digest("base64url");

// RS256: private key signs, public key verifies
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
const rsInput = b64url(JSON.stringify({ alg: "RS256", typ: "JWT", kid: "2026-10-a" })) + "." + payload;
const rsSig = crypto.sign("sha256", Buffer.from(rsInput), privateKey).toString("base64url");
const ok = crypto.verify("sha256", Buffer.from(rsInput), publicKey, Buffer.from(rsSig, "base64url"));

console.log(hsSig.length, rsSig.length, ok); // 43 342 true

The verifying side of the RS256 half needs only publicKey. Hand it to as many services as you like; none of them can produce rsSig. The HS256 half has no such split: whoever can run createHmac to check a signature can run it to make one.

How verifiers find the public key: JWKS and kid

Public keys are usually distributed as a JSON Web Key Set (JWKS, RFC 7517), a JSON document listing keys. OpenID Connect providers advertise its URL as jwks_uri in their discovery document at /.well-known/openid-configuration. A trimmed example, with the 342-character modulus n shortened:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "2026-10-a",
      "use": "sig",
      "alg": "RS256",
      "n": "rRcDfnIDkz3p-SphZlBh…",
      "e": "AQAB"
    }
  ]
}

The token header names its key with kid. The verifier looks that ID up in its cached copy of the set, and refetches the set when it meets an unknown kid, with a rate limit so junk tokens can't make it hammer the endpoint. That is what makes rotation painless: publish the new key next to the old one, start signing with the new kid, and remove the old key once the longest-lived token it signed has expired.

Fetch keys only from the issuer you configured. A token can carry a jku header pointing at a key set URL, but whoever made the token controls that URL, so ignore it unless it is on an allow-list.

HMAC keys: at least 256 bits for HS256

RFC 7518 says an HMAC key must be at least as long as the hash output: 256 bits (32 bytes) for HS256, 384 bits for HS384 and 512 bits for HS512. Length alone isn't the point, though; randomness is. Anyone holding a single token can guess secrets offline, testing each guess against the signature without touching your server, so a dictionary attack finds secret, changeme or your company name quickly. RFC 8725 says human-memorable passwords must not be used directly as HMAC keys.

Generate the key instead:

openssl rand -base64 32
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"

Watch how each side turns the key into bytes. The JWT Builder uses the secret field as raw UTF-8 text, warns when it is shorter than 32, 48 or 64 bytes for HS256, HS384 or HS512, and its Generate button produces a random 256-bit value encoded as Base64URL. If your server base64-decodes its configured secret before using it, the two sides are working with different bytes, and every signature will mismatch even though the strings look identical.

Which one should you choose?

  • Choose HS256 when the same service issues and verifies the tokens, or when two services you control share a secret stored in a secret manager. It is simple, needs no key distribution and produces the shortest signatures.
  • Choose RS256 when several services or third parties verify tokens, when verifiers shouldn't be able to mint tokens, or when you use an identity provider. RS256 is OpenID Connect's default algorithm for ID tokens, so it is the one you will meet most often.
  • Consider ES256 (ECDSA with P-256) when you want the asymmetric model with much smaller keys and signatures: 64 bytes, or 86 characters. Check that every verifier supports it first.

Whichever you pick, give each key exactly one algorithm and make verifiers enforce it. A verifier that lets the token's header choose the algorithm is open to the RS256-to-HS256 confusion attack described in JWT decoder vs JWT verifier.

Next steps

Sign a few HS256 tokens in the JWT Builder, then decode an RS256 token from your identity provider in the JWT Decoder and compare the headers and signature lengths. For a refresher on the claims inside, see how to read JWT claims.

Paste anything. devprix picks the tool.

Press ⌘K on any page, paste JSON, a JWT, a timestamp or a URL, and the right tool opens with your data filled in.