What is a JSON Web Token (JWT)?
A JSON Web Token (JWT), defined in RFC 7519, is a compact way to carry signed claims between two parties. It has three parts separated by dots: a header saying which algorithm signed it, a payload of claims such as who the user is and when the token expires, and a signature over the first two parts. Each part is Base64url-encoded, so a JWT fits neatly in an HTTP header: Authorization: Bearer eyJhbGciOi….
Because the token is signed, a server can verify it with only the key, with no database lookup, and then trust the claims inside. Registered claims include iss (issuer), sub (subject), aud (audience), exp (expiry), nbf (not before), iat (issued at) and jti (token ID). Signing uses either a shared secret (HS256, which is HMAC-SHA256) or a key pair (RS256, ES256, EdDSA), where only the issuer holds the private key and anyone can verify with the public key, often published as a JWKS.
JWTs appear as OpenID Connect ID tokens, single-page-app sessions, service-to-service credentials and the API tokens of many SaaS platforms. They differ from Meta's Graph API access tokens, which are opaque strings that only Meta can interpret: you inspect those with Meta's debug_token endpoint, not by decoding them. A JWT is a format, not a protocol; OAuth 2.0 and OpenID Connect decide when tokens are issued, and a JWT is one way of writing them down.
Key takeaways
- A JWT is header.payload.signature: three Base64url-encoded parts joined by dots.
- The payload is only encoded, not encrypted: anyone holding the token can read its claims.
- Verify the signature with an algorithm you pin, then check exp, nbf, iss and aud before trusting any claim.
- HS256 signs and verifies with one shared secret; RS256 and ES256 sign with a private key and verify with a public one.
- Meta's Graph API access tokens are opaque strings, not JWTs, so never try to decode them.
Edit a JWT and watch its signature break
Incoming request
As signedPOST /webhook HTTP/1.1
JWT signature (HS256): sha256=8b0e326ae2abbb9a1cdd7f4a85112b2897d3f84cff8520cec276880604af4c6e
127 bytes, exactly as the sender signed them.
Your server verifies
- Read the raw body and the headerUse the exact bytes received — 127 bytes, before any JSON parsing.
- Recompute the HMACHMAC-SHA256(secret, raw body)
- Compare the two digestsUse a constant-time comparison.
demo-app-secretJWT signature (HS256) (received) vs HMAC-SHA256(secret, raw body) (computed)
sha256=8b0e326ae2abbb9a1cdd7f4a85112b2897d3f84cff8520cec276880604af4c6esha256=8b0e326ae2abbb9a1cdd7f4a85112b2897d3f84cff8520cec276880604af4c6eMatch — signature valid. The body is byte-for-byte what the sender signed. Process the event.
How does a JSON Web Token (JWT) work?
The issuer builds header and claims
For example
{"alg":"HS256","typ":"JWT"}and{"sub":"agent_42","role":"agent","iat":1792396800,"exp":1792397700}: a token valid for 15 minutes.It encodes and signs
Header and payload are Base64url-encoded and joined with a dot. The signature is computed over that string (HMAC-SHA256 for HS256) and appended as the third part.
The client sends it as a bearer token
Usually in
Authorization: Bearer <jwt>, or for browser sessions in an HttpOnly, Secure, SameSite cookie.The server verifies the signature
It checks the signature with an algorithm fixed in its own configuration, never the one the token's header asks for, using the shared secret or the issuer's public key.
Then it validates the claims
Reject the token if
exphas passed,nbfis in the future, orissandaudare not the expected values. Only then usesuband any roles.
A JSON Web Token (JWT) in code
import jwt from "jsonwebtoken";const SECRET = process.env.JWT_SIGNING_KEY; // 32+ random bytes, from your secret store// Issue a 15-minute agent session tokenexport const issue = (agentId) =>jwt.sign({ sub: agentId, role: "agent" }, SECRET, {algorithm: "HS256",expiresIn: "15m",issuer: "https://dashboard.example.in",audience: "inbox-api",});// Verify: pin the algorithm, check issuer and audience, allow a little clock skewexport function readSession(token) {try {return jwt.verify(token, SECRET, {algorithms: ["HS256"], // never "none", never whatever the header saysissuer: "https://dashboard.example.in",audience: "inbox-api",clockTolerance: 30, // seconds});} catch {return null; // expired, bad signature, wrong audience…}}
jwt.verify checks the signature and exp; issuer and audience are only checked if you pass them.
Common wrong turns
Common mistakes with a JSON Web Token (JWT)
Trusting the algorithm in the header
Libraries that honour the token's own
algcan be tricked into acceptingnone, or into verifying an RS256 token as HS256 with the public key as the secret. Pin the allowed algorithms in every verify call.Not checking expiry and audience
A token minted for another service, or one that expired last month, still carries a valid signature. Always validate
exp,issandaud.Putting secrets in the payload
Anyone holding the token can decode it. Keep passwords, API keys, OTPs and unnecessary personal data out of claims.
Long-lived tokens with no way to revoke
A stolen 30-day JWT works for 30 days. Keep access tokens to minutes, refresh them server-side, and keep a deny-list of
jtivalues for emergencies.Storing JWTs in localStorage
Any cross-site scripting bug can read localStorage. For browser sessions, prefer HttpOnly, Secure, SameSite cookies.
Key facts at a glance
A JSON Web Token (JWT): key facts and limits
- Specification
- RFC 7519 (2015); best practices in RFC 8725
- Structure
- header.payload.signature, Base64url, no padding
- Registered claims
- iss, sub, aud, exp, nbf, iat, jti
- Common algorithms
- HS256 (shared secret); RS256, ES256, EdDSA (key pairs)
- Payload confidentiality
- None: encoded, not encrypted (JWE encrypts)
- Meta Graph API tokens
- Opaque strings, not JWTs
Example: Two attacks on an agent session
A Gurugram SaaS company's shared-inbox dashboard issues 15-minute HS256 JWTs to support agents, with aud set to its inbox API and the agent's role as a claim. A penetration tester edits a captured token's role from "agent" to "admin" and re-encodes it: the signature no longer matches and the API answers 401. He then sends a token whose header says alg: none; verification fails because the server accepts only HS256. Meanwhile, the service that calls the WhatsApp Cloud API holds an opaque Meta system user token in a vault, never in the browser and never inside a JWT.
Why does a JSON Web Token (JWT) matter?
JWTs are the default session and service token in many stacks, including the dashboards and internal APIs built around WhatsApp messaging, and their failure modes are well known: unchecked expiry, accepted alg: none, confusion between HS256 and RS256, secrets left in a readable payload. Each has caused real authentication bypasses. Getting verification right once, in a shared library every service uses, protects all of them.
Good practice
Best practices for a JSON Web Token (JWT)
- Generate HS256 keys from at least 32 random bytes: a short secret can be cracked offline from any captured token.
- Publish a JWKS with key IDs (
kid) so RS256 or ES256 signing keys can be rotated without downtime. - Keep claims minimal: a subject ID and roles are usually enough, and the rest can be looked up server-side.
- Inspect Meta access tokens with the Graph API debug_token endpoint rather than trying to decode them.