Understanding JWT signing
Signed JSON, in three base64 chunks.
What goes in each segment, the algorithm zoo (HS/RS/ES/Ed), the alg:none footgun, and when not to reach for JWT.
Three base64url chunks.
A JWT is three base64url-encoded JSON blobs joined by dots:header.payload.signature. Header declares the algorithm. Payload contains the claims. Signature is computed over base64url(header) + "." + base64url(payload). Verifying means recomputing the signature with the same key and comparing — if it matches, the payload was signed by someone who knew the key.
The standard claims.
RFC 7519 reserves seven claim names: iss (issuer), sub(subject), aud (audience), exp (expiry), nbf(not-before), iat (issued-at), jti (unique ID). Custom claims can go alongside but should be namespaced (e.g. https://example.com/role). Verification libraries check exp and nbf automatically; aud and iss should be checked explicitly to prevent cross-service replay.
The algorithm zoo.
HS256/HS384/HS512: HMAC with SHA-2 — symmetric, simplest, shared secret. RS256/RS384/RS512: RSA-PKCS#1v1.5 + SHA-2 — asymmetric, signer holds private, verifier holds public. PS256/PS384/PS512: RSA-PSS — modern probabilistic padding, preferred over PKCS#1v1.5. ES256/ES384/ES512: ECDSA — asymmetric, much smaller keys. EdDSA: Ed25519. The HS family is fine for single-service auth; RS/ES/EdDSA matter when multiple services verify tokens issued by an auth server.
The alg:none footgun.
The spec allowed alg: "none" for unsigned tokens. Early JWT libraries accepted "none" by default during verification — meaning an attacker could craft a token with the algorithm set to "none" and no signature, and the library would say "valid". Modern libraries reject "none" unless explicitly enabled. Always pin the accepted algorithms list when calling verify(); never trust the alg field from the token itself.
A worked sign.
Header: {"alg":"HS256","typ":"JWT"}. Payload:{"sub":"u123","exp":1748736000}. Encode each as base64url:eyJhbGc....eyJzdWI.... HMAC-SHA256 the concatenation with the shared secret. Base64url-encode the result and append. Total length ~200-300 characters. Decoding requires no key (it's all base64); verifying requires the key. Anyone can read a JWT — don't put secrets in it.
HS256 token
header.payload.signature
3 base64url chunks separated by dots.
HMAC-SHA256(base64(header) + '.' + base64(payload), secret)
= ~250 characters total
When not to use JWT.
Sessions. JWTs can't be revoked without an extra blocklist — a stolen token works until exp. Long-lived auth is better with opaque session IDs in a server-side store. Server-to-server auth between two services on the same network — mTLS is simpler. Anything that needs frequent revocation — API keys with a database row are cleaner. JWT is best for short-lived (minutes-hours) stateless tokens between distributed services, especially when there are multiple verifiers.