ToolsOps

JWT best practices: header, payload, signature and common pitfalls

What lives inside a JWT, how decoding differs from verifying, how HS256, RS256 and ES256 compare, what to do about expiration, why `alg: none` still causes incidents, and where to store the token.

What a JWT is

A JWT (JSON Web Token, RFC 7519) is a compact, self-contained token made of three dot-separated parts: a header, a payload, and a signature. The header declares which algorithm was used to sign, the payload holds the claims (token data: who issued it, about which user, until when it is valid), and the signature guarantees the content has not been altered since the issuer signed it. All three parts are base64url text, so a JWT is trivial to transmit over HTTP or include in an Authorization header.

The standard JWT (JWS Compact Serialization, RFC 7515) does not encrypt the payload, it only signs it. Anyone with the token can read its content. What it provides is authenticity: if the signature is valid with the right key, you know that the issuer produced exactly that token. If you also need confidentiality, JWE encrypts in addition to signing; it is a different 5-part format that this tool does not support in v1.

Anatomy: header, payload and signature

The header is a tiny JSON that almost always contains alg (signing algorithm) and sometimes typ: JWT, kid (key identifier when multiple keys exist) or cty. The payload is another JSON with the token's claims. Registered claims (RFC 7519 §4.1) include iss, sub, aud, exp, nbf, iat and jti. You can add your own claims as long as they do not collide with the reserved ones.

The signature is computed over the concatenation header_b64url.payload_b64url using the algorithm declared in the header. If someone changes a single character of the header or payload, the signature no longer matches and verification fails. That is why trusting a JWT means running that verification, not just reading its content.

Decoding is not verifying

The most common confusion when starting with JWT. Decoding a token is as simple as base64url-decoding the header and payload and reading the JSON. Anyone can craft a JWT with arbitrary claims; the only thing separating a valid token from a forged one is whether the signature matches the key your system considers authoritative. If your code reads the payload and trusts a roles field without verifying the signature, it is trusting a field that anyone could have written.

The ToolsOps JWT decoder explicitly separates the two tabs: Decode shows the content, Verify signature checks against your secret or public key. The default state is always “not verified” until you provide a key.

HS256 vs RS256 vs ES256

All three sign with SHA-256, but they use different cryptographic primitives:

  • HS256 (HMAC with SHA-256): signing and verification both use the same shared secret. Useful when issuer and verifier are the same application or belong to the same organisation with a shared secret. If the verifier is a third party, sharing the secret stops being viable.
  • RS256 (RSASSA-PKCS1-v1_5 with SHA-256): sign with the private key, verify with the public key. De facto standard for identity providers (Auth0, Cognito, Keycloak, Azure AD): they sign with their private key, everyone verifies with the public key published in their JWKS. Long signature, fast verification.
  • ES256 (ECDSA over P-256 with SHA-256): same public/private model as RS256 but with elliptic curves. Shorter signatures (64 bytes for ES256 vs 256 bytes for RS256 at 2048 bits) and faster verification. ES384 and ES512 use curves P-384 and P-521 respectively. Note: the name ES512 refers to SHA-512; the curve is P-521, not P-512.

Rule of thumb: HS256 within a single system; RS256 or ES256 when third parties verify. EdDSA (Ed25519) is appearing in modern identity providers but Web Crypto API support is not universal across browsers in 2026; that is why the tool does not include it in v1.

Expiration, nbf and iat

exp is the timestamp (seconds UTC) after which the token stops being valid. nbf is the timestamp before which the token is not yet active (rare in practice). iat is when the token was issued, useful for computing age or spotting very old tokens that do not match the expected expiration policy.

A server respecting RFC 7519 §4.1.4 must reject a token with exp in the past, even if the signature is valid. The cryptographic signature guarantees the token has not been altered, but does not guarantee it is still acceptable: exp is the temporal boundary. Allowing some clock skew (30-60 seconds) is common to tolerate unsynchronised clocks between issuer and verifier.

`alg: none` and RS/HS confusion

Two classic attack vectors against poorly implemented JWT stacks. alg: none is a permitted value in RFC 7515 §4.1.1 for unsigned tokens. When a library accepts that value without application code blocking it, an attacker can forge a token with any payload and an empty signature, and the server accepts it as valid. The tool actively rejects such tokens: it never marks them as a valid signature.

RS/HS confusion is subtler. If your server is configured to verify RS256 with an RSA public key, and the library lets the algorithm be read from the header, an attacker can send a token with header alg: HS256 signed with HMAC using that same public key as the secret. The verifier tries HS256 with the public key as the secret, the signature matches, and the token passes. Mitigation: never let the verification algorithm depend on the header; pin it in the server code.

Where to store a JWT in the browser

The most common question. Options are HttpOnly + Secure + SameSite cookie, application memory, or localStorage. Rule of thumb: avoid localStorage. Any JavaScript on your origin can read it, including code running via a successful XSS.

  • HttpOnly + Secure + SameSite Lax or Strict cookie: safest option. JavaScript cannot read it, so an XSS cannot exfil the token. Requires server/client coordination so the cookie ships with every API request.
  • Memory (JS variable): the token lives only during the current browser session. Lost on reload. Combined with a refresh token in an HttpOnly cookie, it is a common pattern in SPAs.
  • localStorage: persistent, JS-accessible. Avoid for tokens.

Common mistakes

  • Trusting the payload without verifying the signature. The tool explicitly reminds you of this in the UI.
  • Not validating exp, iss and aud. A valid signature does not imply the token is acceptable to your system.
  • Accepting alg: none without explicit checks in code.
  • Letting the verification algorithm come from the header. Pin the algorithm in code.
  • Storing the token in localStorage instead of an HttpOnly cookie.
  • Reusing the same JWT for multiple APIs with different audiences without separating issuance.
  • Logging the full JWT on errors. Logs usually end up in aggregators with less access control than the API itself.

How to use the ToolsOps decoder

The ToolsOps JWT decoder and verifier accepts a pasted token, splits it into its three parts and decodes header and payload with syntax highlight and temporal claims rendered in your local time. To verify the signature, switch to the Verify signature tab and paste your HMAC secret (UTF-8, hex or base64), your public key in PEM, or your JWK. Verification runs locally with Web Crypto API and no request carries the token, the secret, or the key.

For file download integrity (not tokens), the sibling tool is the hash and checksum calculator (Security cluster). The underlying concepts are covered in the hash functions guide. The security hub groups both tools and guides.

Frequently asked questions

Is a JWT encrypted?
No. A standard JWT (JWS) base64url-encodes the payload, it does not encrypt it. Anyone with the token can read the content. What JWT provides is integrity and authenticity through a signature; without the right key you cannot produce a valid token, but you can still read one. For encrypted payloads there is JWE, a different 5-part variant this tool does not support in v1.
How long should an access token last?
Common practice is 5 to 15 minutes, combined with a long-lived refresh token stored in an HttpOnly cookie. Longer tokens expose more surface if leaked; shorter tokens require more renewals. For internal systems without refresh, 1 hour is reasonable. For critical systems, consider 5 minutes.
Do I always need RS256 with an identity provider?
If the verifier is a third party (another company, another app, a public client), yes: RS256 or ES256 let anyone verify with the public key without sharing a secret. If issuer and verifier are the same application (a monolithic backend signing for itself), HS256 with a shared secret is valid and simpler.
What is the difference between JWT, JWS, and JWE?
JWS (JSON Web Signature) is what most people call a JWT: a 3-part token (header.payload.signature) with the payload in clear. JWE (JSON Web Encryption) is a 5-part token with the payload encrypted in addition to being signed. JWT is the abstract container defined in RFC 7519 pointing at one of the two representations; in practice it almost always means JWS compact.
How do I know which key to use for verification?
If the header includes a `kid` claim, it selects the right key among several published in a JWKS. If your identity provider exposes a `.well-known/jwks.json` endpoint, it contains an array of keys each with its `kid`; pick the JWK whose `kid` matches and use it. If there is no `kid`, there is a single active key.
Can I trust the `sub` claim without verifying the signature?
No. Without verifying the signature, the claims are data anyone could have written. Decoding shows what the token says, not whether it is true. Before using `sub`, `aud`, `roles`, or any other claim for authorization decisions, confirm the signature is valid with the right key.
Why do so many guides say not to use localStorage for JWT?
Because localStorage is accessible by any JavaScript running on the origin, including code injected by an XSS attack. If an attacker manages to execute JavaScript on your page (via a compromised dependency, user-generated content poorly filtered, a malicious extension), they can read the JWT and send it to their server. HttpOnly cookies are not accessible from JavaScript, which limits the impact of XSS at least with regard to the token.
What happens when the identity provider rotates the key?
Tokens issued with the old key stop verifying against the new one. If your application caches the JWKS, it must refresh periodically or whenever a verification fails. Some identity providers expose a specific `kid` per key: if the header has a `kid` you do not know, refetch the JWKS before marking the token as invalid.