BlogDeveloper

JWT signing algorithms compared: HS256, RS256, ES256, EdDSA

The trade-offs between the four JWT signing algorithms that matter in 2026, what each one means for key distribution, and the algorithm-confusion attacks that still appear in real systems.

A JWT header carries an alg field, and the choice it announces shapes how the token is signed, how a consumer verifies it, and what infrastructure has to exist to make that verification possible. There are dozens of algorithms in the IANA registry; in practice four cover ~99% of real systems. This post walks them, explains the operational consequences, and ends on the algorithm-confusion attack that still appears in production code.

The four that matter

Algorithm Family Key type Signature size Where you see it
HS256 HMAC-SHA256 symmetric (shared secret) 32 bytes Internal services, simple SaaS APIs
RS256 RSA-PKCS1-SHA256 asymmetric (RSA 2048+) 256 bytes Auth0, AWS Cognito default, OIDC ID tokens
ES256 ECDSA-P256-SHA256 asymmetric (P-256) 64 bytes Apple, Google services, modern IdPs
EdDSA Ed25519 asymmetric (Curve25519) 64 bytes OAuth GNAP, Solid pods, newer protocols

Two further algorithms appear in older systems: HS512 (HMAC with SHA-512, identical trade-offs to HS256 with double-size output) and RS512 (same caveats as RS256). The IANA registry also lists PS256 (RSA-PSS), which is cryptographically preferable to PKCS1 but rarely chosen in practice because the operational benefit is small and ecosystem support is patchier.

HS256: when symmetric keys make sense

HS256 signs the token with a single shared secret. Whoever can verify the token can also forge new ones. That constraint is fine when issuer and verifier are the same service — internal API token signing for example, where one binary issues a session and validates it on each request — but breaks down the moment a third party needs to verify without also being able to mint tokens.

The operational appeal of HS256 is its simplicity. The key is one random string from your secrets manager; rotation is one secret update; the verification math is two hash operations and a constant- time compare. Tokens are small (32-byte signature) and fast to process (sub-millisecond on commodity hardware).

The trade-off is key distribution. Every service that needs to verify the token holds the signing power. For a single service this is fine. For five microservices that should be able to verify a session token but only one of which should be able to issue them, HS256 is the wrong choice — the other four end up with the secret, and a compromise of any of them is a compromise of issuance.

RS256: the public-key default

RS256 splits issuance from verification. The issuer holds a private RSA key; anyone holding the matching public key can verify tokens without being able to mint them. This is what made OIDC viable — identity providers publish their public keys at a well-known JWKS URL and arbitrary relying parties verify ID tokens without prior coordination.

The operational complexity comes with that flexibility. Keys need to be 2048 bits or larger (AWS, Auth0 and most major IdPs default to 2048 or 4096). Signatures are 256 bytes — eight times the size of HS256 — and verification is materially slower (single-digit ms instead of microseconds). For a service handling 50,000 RPS the CPU cost of RSA verification is observable on a flame graph.

Key rotation is also harder. You publish a JWKS endpoint with both the old and new public keys for the overlap window, sign new tokens with the new key, and remove the old key only after the longest- lived outstanding token has expired. Most JWT libraries can read multi-key JWKS by default, but a non-trivial percentage of implementations cache the JWKS aggressively and miss rotations.

ES256: shorter keys, smaller signatures, same model

ES256 uses ECDSA over the P-256 elliptic curve. The cryptographic properties are equivalent or stronger than RS256: 256-bit ECC provides roughly the security of 3072-bit RSA. The operational benefits are substantial. Keys are 256 bits (32 bytes), signatures are 64 bytes — four times smaller than RS256. Verification is faster. Signing is faster too, though for IdPs the verification side is what matters at scale.

The downsides are ecosystem-level. ECDSA implementations have historically had more subtle bugs than RSA implementations (the Sony PS3 ECDSA reuse of nonce, the Java 15 EC_KEY parser CVE) and mitigation requires a careful library choice. Hardware support for P-256 in TLS terminators and HSMs is universal in modern infrastructure but spottier in old appliances. The signature itself is not deterministic — verifying side has to be tolerant of different signatures over the same payload because the random nonce changes.

If you are choosing an asymmetric algorithm for a new service and your platform supports it, ES256 is the modern default.

EdDSA: the newest choice

EdDSA with Ed25519 is the most recent addition. It is deterministic (same payload → same signature, no nonce dependency), fast on commodity CPUs without hardware acceleration, and immune to several classes of side-channel attack that ECDSA needs constant- time implementations to defend against. RFC 8037 added EdDSA to JOSE in 2017.

Library support has caught up but is not yet universal. node-jose, PyJWT, and jose (the most popular JavaScript JWT library) all support it; some enterprise IdPs and SDKs do not. For greenfield designs targeting modern stacks, EdDSA is the recommended choice; for interoperability with the broader ecosystem, ES256 remains the safer pick.

Algorithm confusion attacks

The most serious JWT vulnerability in real systems is not in any algorithm but in implementations that trust the alg header. Two attack shapes still appear in audits.

The first is alg=none. JWS specifies a "none" algorithm that produces a token with an empty signature, intended for use cases where the signature is unnecessary. Implementations that decode the header, look up the algorithm, and dispatch to a verifier without checking that "none" is acceptable will happily verify a forged token. The fix is to maintain an explicit allow-list of algorithms on the verifier side, with none deliberately excluded.

The second is RS256 → HS256 confusion. A verifier configured to accept RS256 tokens holds the issuer's RSA public key. An attacker crafts a new token claiming alg: HS256 and signs it using the public key as if it were a shared secret. A naive implementation will use whichever key it has on hand for the algorithm declared in the header, treating the RSA public key as an HMAC key, and accept the forgery. The fix is again the allow-list: pin both the algorithm and the key type at verifier configuration time.

When integrating a JWT library, the question to ask is not "does it support RS256" but "does the verifier validate the alg field against my allow-list before dispatching." Major libraries (jose, jsonwebtoken, PyJWT, node-jose) have evolved to require an explicit algorithm pin in the verify call; older code paths or in-house implementations still sometimes skip it.

Choosing for a new service

For an internal-only API where issuer and verifier are the same binary, HS256 is fine and the simplest choice. For anything that needs third-party verification, asymmetric is required: ES256 if your platform supports it cleanly, EdDSA for greenfield modern stacks, RS256 for maximum interoperability with legacy systems.

Whatever you pick, pin the algorithm on the verifier side, publish the public-key material at a stable JWKS URL with sane caching headers, and budget for a key rotation drill before you need one. For inspecting tokens in the field, our JWT Decoder shows the header and payload without attempting verification — useful when debugging an auth flow without needing the verifier's secret on hand.