HS256 vs RS256
On this page
HS256 is HMAC with SHA-256. It signs a JWT with one shared secret, and the same secret verifies it. RS256 is an RSA signature with SHA-256. A private key signs the token and the matching public key verifies it. Both put the same kind of token on the wire. What differs is who holds which key.
The short answer: if one service signs and verifies its own tokens, HS256 is fine, as long as the secret is long and random. If other services or third parties need to verify tokens, use RS256 (or ES256) so they only ever get a public key. The table below sums it up.
You can paste any token into the JWT decoder to see its alg.
It runs in your browser. Nothing is uploaded, and there’s no signup.
What is HS256?
HS256 is the JWT name for HMAC using SHA-256. It’s defined in RFC 7518, section 3.2, the JSON Web Algorithms spec. It’s the one algorithm every JWS implementation is required to support.
HMAC mixes a secret key into a SHA-256 hash of the signing input, which
is base64url(header) + "." + base64url(payload). The 32-byte result is
the signature. To verify, the receiver runs the same HMAC with the same
secret and compares the two values. RFC 7518 says that comparison must
be constant-time, which any decent library already does for you.
The key point is that there’s one key. Whoever can verify an HS256 token can also create one. RFC 7515 section 10.5 spells out what that means: a valid MAC only shows the token came from someone holding the secret. It can’t prove which party that was.
The “256” is SHA-256. HS384 and HS512 are the same algorithm with SHA-384 and SHA-512.
What is RS256?
RS256 is RSASSA-PKCS1-v1_5 using SHA-256 (RFC 7518, section 3.3). It’s the classic RSA signature scheme from PKCS #1. The issuer signs with its private key, and anyone with the matching public key can verify.
That split is the whole point. The public key can verify tokens but can’t create them, so you can hand it to every service, partner or client that needs to check a token. Only the issuer holds the private key.
HS256 vs RS256 at a glance
| HS256 | RS256 | |
|---|---|---|
| Full name | HMAC using SHA-256 | RSASSA-PKCS1-v1_5 using SHA-256 |
| Key type | One shared secret | RSA key pair |
| Who can sign | Anyone with the secret | Only the private-key holder |
| Who can verify | Anyone with the secret | Anyone with the public key |
| Minimum key size (RFC 7518) | 256 bits | 2048 bits |
| Signature size | 32 bytes (43 base64url chars) | Same as the key: 256 bytes for 2048-bit (342 chars) |
| Key distribution | Secret must reach every verifier privately | Public key can be published, e.g. in a JWKS |
| Good fit | One service signs and verifies its own tokens | Many services or third parties verify |
| JWA implementation requirement | Required | Recommended |
The last row is what the JWA spec (and the IANA JOSE registry) asks libraries to support. It’s not advice on which one you should pick.
When to use HS256
HS256 fits when the signer and the verifier are the same system, or a very small set of systems you fully control:
- A single backend that issues a session-style token at login and checks it on the next request.
- A monolith, or a couple of services that deploy together and already share secrets through the same secret store.
- Short-lived internal tokens where both ends are yours.
It gets awkward as soon as the circle grows. Every verifier needs the secret, and every verifier can therefore mint tokens. A leak from any one of them compromises all of them. Rotating the secret means updating every holder at once.
When to use RS256
Reach for RS256 (or the other public-key algorithms below) when tokens cross a trust boundary:
- Many services verify. An auth service signs, and a dozen APIs verify with the public key. None of those APIs can forge a token.
- Third parties verify. Partners, customers or mobile apps can check your tokens without you sharing anything secret.
- You consume tokens from an identity provider. OpenID Connect providers sign ID tokens with a private key and publish the public keys. You never see their signing key.
Publishing keys with JWKS
Public keys are usually published as a JWK Set
(RFC 7517, section 5).
That’s a JSON document with a keys array. Each key can carry a kid
(key ID), a use of "sig", and an alg:
{
"keys": [
{
"kty": "RSA",
"kid": "2026-09",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuu…",
"e": "AQAB"
}
]
}
(n is the modulus, cut short here. e of AQAB is 65537.)
The token header names the key it was signed with:
{ "alg": "RS256", "typ": "JWT", "kid": "2026-09" }
Where do verifiers find the JWKS? That’s up to the issuer. With OpenID
Connect, the issuer’s discovery document lists it as jwks_uri.
RFC 8725 section 3.8 says the keys you verify with must belong to the
issuer, so configure the JWKS URL yourself. Don’t take it from the token.
Rotating keys with kid
RFC 7515 describes kid as a hint that lets the signer “explicitly
signal a change of key.” A typical rotation goes like this:
- Add the new public key to the JWKS under a new
kid, next to the old one. - Give verifiers time to refresh their cached copy.
- Start signing with the new private key.
- Remove the old key once every token signed with it has expired.
Treat kid as untrusted input. RFC 8725 section 3.10 warns that it can
carry SQL or LDAP injection if you use it in a lookup. It also says
blindly following a jku or x5u URL from the header invites SSRF.
HS256 has no public half to publish. Rotating means getting the new secret to every verifier through a private channel.
Key sizes
HS256: at least 256 bits. RFC 7518 section 3.2: “A key of the same size as the hash output (for instance, 256 bits for “HS256”) or larger MUST be used with this algorithm.” HS384 needs 384 bits and HS512 needs 512.
256 bits is 32 bytes, and it means 32 random bytes, not 32 typed characters. RFC 8725 section 3.5 says human-memorable passwords must not be used directly as an HS256 key. Generate the secret instead:
# 32 random bytes, printed as 44 base64 characters
openssl rand -base64 32
Whatever form you store it in, the signer and every verifier have to use the exact same bytes. A secret that’s base64-decoded on one side and used as a plain string on the other won’t match.
RS256: at least 2048 bits. RFC 7518 section 3.3: “A key of size 2048 bits or larger MUST be used with these algorithms.” NIST SP 800-57 Part 1 (Rev. 5) rates 2048-bit RSA at 112 bits of security and 3072-bit at 128 bits. Rev. 5 accepts 112 bits for new signatures through 2030. If your keys need to outlive that, 3072 bits is the next step.
# RSA private key (2048-bit) and its public key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem
Keep private.pem on the signer only. public.pem, or its JWK form,
is the part you share.
Signature and token size
The signature is the third segment of the token. Its size depends only on the algorithm and key, not on the payload:
| Algorithm | Signature | Base64url in the token |
|---|---|---|
| HS256 | 32 bytes | 43 characters |
| RS256, 2048-bit key | 256 bytes | 342 characters |
| RS256, 3072-bit key | 384 bytes | 512 characters |
| ES256 | 64 bytes | 86 characters |
An HMAC-SHA-256 output is always 32 bytes. An RSA signature is always as long as the modulus (RFC 8017), so a 2048-bit key gives 256 bytes. Switching a token from HS256 to RS256 with a 2048-bit key adds about 300 characters to every request that carries it. That usually doesn’t matter. It can matter for tokens sent in cookies or headers close to a size limit.
The decoder shows the algorithm and the signature length in bytes under any token you paste.
On speed: HMAC is a keyed hash, so it’s cheap both ways. With RSA, signing with the private key is the expensive side, and verifying with the public key is much lighter. For most APIs neither is the bottleneck. Choose on key distribution, not speed.
Common mistakes
Short or guessable HS256 secrets
This is the big one. Anyone who gets hold of a single HS256 token has
everything needed to test guesses offline: the signing input and the
signature. No requests to your server, no rate limits. RFC 8725
section 2.2 calls weak HMAC keys “vulnerable to offline brute-force or
dictionary attacks once an attacker gets hold of such a token.” If the
secret is a word, a phrase, a default like secret, or a value
from a tutorial, assume it’s guessable. Use 32+ random bytes.
Algorithm confusion (RS256 → HS256)
Your service signs with RS256, and the public key is, by design,
public. An attacker builds a token with "alg": "HS256" and computes an
HMAC over it, using your public key as the HMAC secret. A verifier
that reads alg from the header and passes “the key” to whatever
algorithm the header names will run HMAC with the public key. It
matches, and the forged token is accepted.
RFC 8725 section 2.1 describes exactly this, citing CVE-2015-9235. The fix in section 3.1: the caller specifies the allowed algorithms, and “each key MUST be used with exactly one algorithm.”
Accepting alg: none
none is a registered value that means “no signature”
(RFC 7518, section 3.6).
The spec says implementations “MUST NOT accept Unsecured JWSs by
default.” Some libraries used to accept them anyway, and an attacker
could strip the signature and set alg to none (RFC 8725 section
2.1). If your verifier accepts none, it isn’t verifying.
Not pinning the algorithm on verify
Both attacks above come from the same root cause: letting the token pick its own algorithm. RFC 7519 section 7.2 says it’s “an application decision which algorithms may be used,” and a token using any other algorithm should be rejected. Pass an explicit allow-list every time, even if your library’s default happens to be safe today.
// Example uses the jose library for Node.js. Most JWT libraries have an
// equivalent "algorithms" option.
import { createRemoteJWKSet, jwtVerify } from "jose";
// RS256: verify with the issuer's published public keys
const JWKS = createRemoteJWKSet(new URL("https://auth.example.com/jwks.json"));
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "api.example.com",
});
// HS256: verify with the shared secret
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const result = await jwtVerify(token, secret, { algorithms: ["HS256"] });
# Example uses PyJWT
import jwt
payload = jwt.decode(token, public_key, algorithms=["RS256"], audience="api.example.com")
Treating decode as verify
Decoding a JWT just base64url-decodes the header and payload. It checks
nothing. RFC 7519 section 7.2 says a JWT that fails any validation step
“MUST be rejected,” and RFC 8725 section 3.3 says every cryptographic
operation must be validated. Reading sub out of a token you haven’t
verified is trusting whatever the sender typed. The full breakdown is
in JWT decode vs verify.
What about ES256 and PS256?
ES256 is ECDSA using the P-256 curve and SHA-256 (RFC 7518, section 3.4). It uses the same model as RS256: a private key signs and a public key verifies, and you can publish it in a JWKS the same way. The keys and signatures are much smaller. A P-256 key gives about the same security as 3072-bit RSA (NIST SP 800-57), and an ES256 signature is 64 bytes against 256 for RS256 with a 2048-bit key. The JWA spec lists it as “Recommended+”, meaning its requirement level is likely to go up. ECDSA needs a fresh random value for every signature. RFC 8725 section 3.2 asks libraries to use the deterministic form from RFC 6979. Use a maintained library and don’t write this yourself.
PS256 is RSASSA-PSS with SHA-256 (RFC 7518, section 3.5). It uses the same RSA keys (2048 bits or larger) and the same signature size as RS256, but with the newer PSS padding. Pick one per key. RFC 8725 says each key gets exactly one algorithm.
Ed25519 is also registered for JWS. The older generic EdDSA name is
now marked deprecated in the IANA registry (RFC 9864). Check which name
your library and your verifiers expect.
Frequently asked questions
Is HS256 secure?
Yes, when the secret is at least 256 bits of real randomness and stays private. Almost every HS256 break is a weak or leaked secret, not a flaw in HMAC-SHA-256. The other risk is structural: every verifier can also sign.
Can I verify an HS256 token with a public key?
No. HS256 has no public key. Verifying needs the same secret that signed the token. If someone asks you for a “public key” for an HS256 token, the token is using the wrong algorithm for the job.
Is RS256 encryption?
No. RS256 signs the token. It doesn’t hide anything. The header and payload are base64url-encoded and readable by anyone holding the token, whichever algorithm signed it. If the contents must be confidential, you need JWE (encrypted JWTs) or should keep sensitive data out of the token.
Should I use RS256 or ES256 for a new system?
Both use the same public/private model and both are fine. ES256 gives smaller keys and tokens. RS256 is the older and very widely supported choice. Check what your identity provider, gateways and client libraries support, then pin that one algorithm.
How do I move from HS256 to RS256?
Generate an RSA key pair and publish the public key (a JWKS with a
kid works well). Then update verifiers to accept RS256 for tokens
signed with the new key. Switch the issuer to RS256, and remove HS256
from every allow-list once the last HS256 token has expired. Don’t
configure a single verifier to accept both algorithms with the same key
material. That’s the setup algorithm confusion exploits.
Can Tooljo’s tools check an RS256 signature?
Not today. The decoder reads any JWT, whatever the algorithm. Verify checks HS256, HS384 and HS512 signatures, and Sign creates HS256 test tokens. For RS256 or ES256, verify in a server-side library with the algorithm pinned, as in the snippets above.
Try it
- JWT decoder: paste a token to see its
alg,kidand claims, plus the signature size. - Verify HMAC: check an HS256, HS384 or HS512 signature against a secret.
- Sign HS256: make an HS256 test token with your own claims.
Everything runs in your browser with the Web Crypto API. Nothing you paste is sent anywhere. Use test secrets, not production keys.
Related
- JWT decode vs verify: why reading a token isn’t trusting it.
- JWT standard claims: the
exp,issandaudchecks to run after the signature passes. - What is a JWT?: header, payload and signature basics.
- hash.tooljo.com/sha256: the SHA-256 hash inside both HS256 and RS256.
- base64.tooljo.com/url-safe: JWT segments are base64url. Decode the raw bytes.