Decoding a JWT reads the token contents. Verifying a JWT checks the cryptographic signature. They are completely different operations. Using decode when you should verify is one of the most common JWT security mistakes — and it leaves your application wide open to forged tokens.
The confusion comes from JWT libraries that offer both functions with similar-sounding names. Here is the exact difference, when to use each, and the security implications of getting it wrong.
| Decode | Verify | |
|---|---|---|
| What it does | Base64-decodes the header and payload to read the claims | Checks the cryptographic signature against a secret/public key |
| Key required? | \u2717 No key needed | \u2713 Secret key (HMAC) or public key (RSA/ECDSA) |
| Can detect tampering? | \u2717 No — reads whatever is in the payload | \u2713 Yes — rejects tokens with invalid signatures |
| Can detect expired tokens? | ~Only if you manually check the exp claim | \u2713 Most libraries auto-reject expired tokens |
| Trusted for auth decisions? | \u2717 Never — anyone can craft a fake payload | \u2713 Yes — proves the token came from the trusted issuer |
| Where to use | Client-side: UI display, session timeout checks | Server-side: API authentication, authorization decisions |
| Performance | \u2713 Instant — simple Base64 decode | ~Slightly slower — cryptographic hash comparison |
| Example library | jwt-decode (npm), base64 decode (any language) | jsonwebtoken (npm), jose, PyJWT, java-jwt |
Here is what goes wrong when a server only decodes without verifying:
{"role": "user", "sub": "12345"} and signs it with your secret key{"role": "admin", "sub": "12345"} and signs it with a random key (or no key at all)role: admin and grants admin accessIf your server verified the signature, step 4 would fail because the signature would not match the expected value. The forged token would be rejected immediately.
These are all convenience features, not security controls. The client is an untrusted environment — any check here can be bypassed by an attacker. The server must independently verify everything.
| Library | Language | Decode | Verify | Sign | Size |
|---|---|---|---|---|---|
| jwt-decode | JavaScript | \u2713 | \u2717 | \u2717 | ~2KB — decode only |
| jsonwebtoken | JavaScript | \u2713 | \u2713 | \u2713 | ~30KB — full JWT operations |
| jose | JavaScript | \u2713 | \u2713 | \u2713 | ~45KB — modern, supports JWE |
| PyJWT | Python | \u2713 | \u2713 | \u2713 | Full library |
| java-jwt (Auth0) | Java | \u2713 | \u2713 | \u2713 | Full library |
| golang-jwt | Go | \u2713 | \u2713 | \u2713 | Full library |
| System.IdentityModel | C#/.NET | \u2713 | \u2713 | \u2713 | Built into .NET |
Rule: Use a decode-only library on the frontend. Use a full library with verification on the backend. Never use a decode-only library for server-side authentication.
decoded.role without verification, any client can send a forged token with any role. Always verify first, then read claims from the verified payload.Even with proper verification, JWTs have limitations: they cannot be revoked before expiration without additional infrastructure (blacklists or short expiration + refresh tokens), they increase in size as you add claims, and they are stateless by design — the server cannot track individual sessions without additional state. For high-security applications requiring immediate revocation, consider session tokens with server-side storage instead of or alongside JWTs.
Decode any JWT right now — see the header, payload, and expiration instantly.
Open JWT Decoder