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 browserThe 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 usuallytyp: "JWT". Some headers also includekid, 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
| alg | Type | Signs with | Verifies with |
|---|---|---|---|
HS256 | HMAC with SHA-256 | Shared secret | The same shared secret |
RS256 | RSA PKCS#1 v1.5 with SHA-256 | RSA private key | RSA public key |
PS256 | RSA-PSS with SHA-256 | RSA private key | RSA public key |
ES256 | ECDSA on P-256 with SHA-256 | EC private key | EC public key |
EdDSA | Ed25519 signatures | Ed25519 private key | Ed25519 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:
- Reject the token unless
algis one you explicitly allow for this issuer (never read the algorithm from the token and trust it blindly). - Select the key — the shared secret, or the public key matching
kidfrom the issuer's JWKS. - Recompute or check the signature over
header.payload, using a constant-time comparison for HMAC. - Validate the claims:
expis in the future,nbf(if present) is in the past,issis the expected issuer, andaudincludes 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
exporaud— 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,issandaud, and must happen on the server.
More guides
- Cron Syntax Cheatsheet: Fields, Operators and 25 Ready-Made SchedulesA practical reference to the five-field cron format, the special characters, common schedules you can copy, and the gotchas in crontab, GitHub Actions and Kubernetes.
- Fixing "Unexpected token" and Other JSON Parse ErrorsWhat JSON.parse errors like "Unexpected token < in JSON", "Unexpected end of JSON input" and "Expected double-quoted property name" really mean, and how to fix each one.
- Base64 Encoding Explained: How It Works and When to Use ItHow Base64 turns bytes into text, why output is a third larger, what the = padding means, Base64 vs Base64URL, and how to handle Unicode correctly in JavaScript.