artisan.dev

How JWT Signatures Work (HS256 vs RS256, Explained)

By Tahsin Abrar · Updated

A JSON Web Token (JWT) looks like a random string, but it is really three pieces of Base64URL-encoded data joined by dots. Anyone can read the first two pieces. The third piece — the signature — is what makes a token trustworthy, and misunderstanding it is behind most JWT security bugs.

This guide walks through how the signature is produced, how a server checks it, and how the common algorithms differ. You can paste any token into the JWT Decoder while you read to see each part.

Try itOpen the JWT Decoder — runs in your browser

The three parts of a JWT

A compact JWT has the form header.payload.signature:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFkYSIsImV4cCI6MTc5MDAwMDAwMH0.4Hb6v0nJ1o0Y9d3a…
  • Header — JSON describing the token, most importantly alg (the signing algorithm) and usually typ: "JWT". Some headers also include kid, a key ID telling the verifier which key to use.
  • Payload — JSON containing the claims: who the token is about (sub), who issued it (iss), who it is for (aud), when it expires (exp), when it was issued (iat), plus any application data.
  • Signature — bytes computed from the first two parts and a key. It is Base64URL-encoded like the others but is not JSON.

Base64URL is ordinary Base64 with + and / replaced by - and _, and the = padding removed so the token is safe in URLs and HTTP headers. It is an encoding, not encryption: the header and payload can be decoded by anyone who holds the token. Never put secrets in a JWT payload.

How the signature is computed

The signer takes the exact encoded header and payload strings, joins them with a dot, and signs that string:

signingInput = base64url(header) + "." + base64url(payload)
signature    = SIGN(signingInput, key)
token        = signingInput + "." + base64url(signature)

Because the signature covers the encoded bytes, changing a single character in the header or payload — say, flipping "role":"user" to "role":"admin" — produces a different signing input, and the old signature no longer matches. That is the whole point: the payload is readable but tamper-evident.

HS256 vs RS256 vs ES256

algTypeSigns withVerifies with
HS256HMAC with SHA-256Shared secretThe same shared secret
RS256RSA PKCS#1 v1.5 with SHA-256RSA private keyRSA public key
PS256RSA-PSS with SHA-256RSA private keyRSA public key
ES256ECDSA on P-256 with SHA-256EC private keyEC public key
EdDSAEd25519 signaturesEd25519 private keyEd25519 public key

HMAC (HS256/384/512) is symmetric: the same secret creates and checks the signature. It is fast and simple, but every service that verifies tokens also holds the secret and could therefore mint tokens. It fits a single application that issues and consumes its own tokens.

Asymmetric algorithms (RS256, PS256, ES256, EdDSA) sign with a private key that only the issuer holds, and verify with a public key that can be shared freely — often published as a JWKS (JSON Web Key Set) at a URL like /.well-known/jwks.json. This is what identity providers such as Auth0, Okta, Keycloak, Google and Microsoft Entra ID use, because any number of APIs can verify tokens without being able to forge them. ES256 and EdDSA produce much shorter signatures than RSA for equivalent security.

Decoding is not verifying

A decoder — including the one on this site — splits the token and Base64URL-decodes the header and payload so you can read them. It does not prove the token is genuine. Verification requires the key and must happen on the server that trusts the token. A correct verification step:

  1. Reject the token unless alg is one you explicitly allow for this issuer (never read the algorithm from the token and trust it blindly).
  2. Select the key — the shared secret, or the public key matching kid from the issuer's JWKS.
  3. Recompute or check the signature over header.payload, using a constant-time comparison for HMAC.
  4. Validate the claims: exp is in the future, nbf (if present) is in the past, iss is the expected issuer, and aud includes your service. Allow a small clock skew, typically under a minute.

Use a maintained library for this (for example jose in Node.js, PyJWT in Python, or golang-jwt in Go) rather than writing it by hand.

Classic JWT vulnerabilities

  • alg: none — the JWT spec defines an "unsecured" token with no signature. Libraries that accept it let an attacker remove the signature and change any claim. Always pin the allowed algorithms.
  • Algorithm confusion — if a server expects RS256 but also accepts HS256, an attacker can sign a token with HMAC using the server's public key as the secret. Again: pin the algorithm per key.
  • Weak HMAC secrets — HS256 tokens can be brute-forced offline if the secret is short or a dictionary word. Use at least 256 bits of random data; the password generator can produce one.
  • Not checking exp or aud — a valid signature only proves who issued the token, not that it is still valid or meant for you.
  • Sensitive data in the payload — anyone holding the token can read it. If you need confidentiality, use JWE (encrypted JWT) or keep the data server-side.

Inspecting a token safely

Production tokens are credentials. Pasting one into an online decoder that sends it to a server is effectively handing over a session. The JWT Decoder on iamartisan.dev runs entirely in your browser: it decodes the header and payload locally, shows the algorithm, and converts exp and iat into readable dates with a countdown to expiry. Nothing is transmitted. For timestamp claims you can also use the Unix timestamp converter.

FAQ

Can someone read the data inside my JWT?
Yes. The header and payload are only Base64URL-encoded, not encrypted, so anyone with the token can decode them. Treat a JWT payload as public and keep secrets out of it.
Should I use HS256 or RS256?
Use HS256 when the same application both issues and verifies tokens. Use RS256, ES256 or EdDSA when other services need to verify tokens, so they only need the public key and cannot create tokens themselves.
Does decoding a JWT verify it?
No. Decoding just reads the header and payload. Verification checks the signature with the correct key and validates claims like exp, iss and aud, and must happen on the server.

More guides