Developer Guide
Verify JWT HS256 Signatures Locally
Anyone can read a JWT payload. HS256 verify needs the issuer secret and Web Crypto HMAC. RS256 is decoded, not verified. exp/nbf badges are not a signature check.
Decoding a JWT is Base64url plus JSON.parse. Verifying it is HMAC-SHA-256 over header.payload with a secret you already have. Those are different buttons. /tools/jwt-debugger always decodes locally. It verifies only when alg is HS256 and you type the secret. RS256, ES256, and none are shown as unsupported for verify. Time badges for exp / nbf appear after the page mounts and do not mean the signature is valid.
Three parts, none of them encrypted
A compact JWT is base64url(header).base64url(payload).base64url(mac). The first two parts are readable. {"alg":"HS256","typ":"JWT"} is not a secret. Neither is {"sub":"user-1",…}. Treat the payload as a postcard. Encryption would be JWE, which this tool does not open.
Paste must have three segments. One or two parts is an error. Garbage Base64url is an error. An empty field is not an error so the page can sit idle. Header and payload must be JSON objects, not arrays.
What HS256 actually signs
The MAC is HMAC-SHA-256 of the signing input: the first two segments including the dot, UTF-8, with the shared secret. The tool compares the result to the third segment using a length-checked XOR loop (timing-safe on equal lengths) and also accepts a Base64url match. Wrong secret → Signature invalid. Empty secret → need-secret, not a guess.
The sample expired token on the page is signed with thedevtools-hs256. Verify succeeds with that secret even though exp is 1700003600 (2023-11-14 23:13:20 UTC) and the badge says expired. Valid signature, stale claims. Your API must check both.
alg confusion and unsupported algorithms
If header.alg is not the string HS256, verify returns unsupported and names the algorithm (RS256, none, …). That is deliberate. A classic attack is swapping alg to none or to HS256 while the server still has an RSA public key lying around as a “secret”. This debugger will not HMAC-verify a token that does not claim HS256, and it will not import an RSA public key.
You can still read an RS256 payload here. Green “valid” will not appear. Production verification for RS256 belongs in your auth library with the issuer’s JWKS.
exp, iat, nbf — clocks, not signatures
RFC 7519 NumericDate is Unix seconds. Date.now() is milliseconds; divide by 1000. The debugger prints each present claim as Unix, UTC, and local. exp at or before now → expired. nbf in the future → not-yet-valid. Badges stay empty until mount so server HTML does not disagree with the client clock (no hydration mismatch).
A 13-digit exp is almost always milliseconds stuffed into a seconds field. Convert the number on /tools/timestamp. 1700000000 is 2023-11-14 22:13:20 UTC. The sample payload uses iat 1700000000, nbf 1600000000, exp 1700003600.
Where the secret should never go
The HMAC secret is the issuer’s. Pasting it into this tab keeps it in that tab. Pasting it into a meeting recording, a screenshot, or a random website that is not this page is a leak. Rotate if you demonstrated with production material.
/tools/hash-generator HMAC-SHA-256 of the entire compact JWT string will not equal the JWT signature. Different input. Use the debugger for tokens; use the hash tool for webhook bodies and files.
Common pitfalls
Trusting the payload because it decoded. Assuming expired means invalid signature (or the reverse). Verifying HS256 against an RS256 token. Comparing exp to Date.now() without dividing by 1000. HMAC-ing the whole token in the Hash Generator and declaring the JWT broken.
- Decode ≠ verify. Verify is HS256 only.
- Sample secret
thedevtools-hs256validates the sample tokens’ signatures. - Time badges are client-only after mount; they are not the MAC.
Worked examples
Sample HS256 header
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9{
"alg": "HS256",
"typ": "JWT"
}Every HS256 token on this site starts from that header. If alg is RS256, the debugger will decode and refuse to verify.
Expired sample still has a valid MAC
token: the built-in expired sample
secret: thedevtools-hs256verify status: valid (Signature valid)
exp badge: expired (exp 1700003600 = 2023-11-14 23:13:20 UTC)Wrong secret → Signature invalid. Empty secret → enter the HMAC secret. RS256 header → HS256 verification only.
Frequently asked questions
Why is the signature valid on an expired token?
The MAC does not include “now”. It covers header and payload bytes. Expiry is a separate check your server must perform. The debugger shows both.
Can I verify RS256 here?
No. The tool decodes RS256 and other algorithms, then reports unsupported for verify. Use your auth library and the issuer’s public key.
Does thedevtools.in upload the token or the secret?
No. Parse and HMAC run in the browser. Still do not paste production secrets into a recorded call.
Why are the expired badges missing on first paint?
Time badges use the client clock after mount so SSR HTML and the hydrated tree match. They appear a moment later.
The Hash Generator HMAC does not match my JWT signature. Why?
HS256 HMACs the signing input (two segments plus a dot), then Base64url-encodes the MAC. Hashing the whole compact string is a different message.
Summary
Read the postcard (decode), then check the wax seal (HS256) with the issuer secret in /tools/jwt-debugger. Ignore a green MAC if exp is stale, and ignore pretty claims if verify is unsupported or invalid. Convert NumericDate values on /tools/timestamp. Leave RS256 to a real verifier, and do not HMAC the compact token on /tools/hash-generator expecting the same result.