All Tools / Blog / How to Decode a JWT Without Verifying the Signature

How to Decode a JWT Without Verifying the Signature

3 min read

A JSON Web Token (JWT) is just a Base64-encoded JSON object — the signature exists to verify the token's authenticity, but the payload itself is not secret. There are plenty of legitimate reasons to decode a JWT without verifying the signature: debugging, inspecting expiry time, reading the subject claim in a non-auth service, or checking what a third-party token contains.

Here's how to do it safely.

What a JWT looks like

A JWT has three parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • Part 1 (header): algorithm and token type
  • Part 2 (payload): the actual claims — user ID, expiry, roles, etc.
  • Part 3 (signature): HMAC or RSA signature over parts 1+2

The header and payload are just Base64URL-encoded JSON. You can decode them with no key at all.

Decoding in Python

import base64
import json

def decode_jwt_payload(token: str) -> dict:
    # Split and take the payload (second segment)
    payload_b64 = token.split(".")[1]

    # Base64URL has no padding — add it back
    padding = 4 - len(payload_b64) % 4
    if padding != 4:
        payload_b64 += "=" * padding

    decoded_bytes = base64.urlsafe_b64decode(payload_b64)
    return json.loads(decoded_bytes)


token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
payload = decode_jwt_payload(token)
print(payload)
# {'sub': '1234567890', 'name': 'John Doe', 'iat': 1516239022}

The same approach works for the header — just use token.split(".")[0] instead.

Decoding in JavaScript (browser or Node.js)

function decodeJwtPayload(token) {
    const base64Url = token.split('.')[1];
    // Replace URL-safe chars and add padding
    const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    const jsonStr = atob(base64);  // browser / Node 16+
    return JSON.parse(jsonStr);
}

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
console.log(decodeJwtPayload(token));
// { sub: '1234567890', name: 'John Doe', iat: 1516239022 }

In Node.js before v16, use Buffer.from(base64, 'base64').toString('utf8') instead of atob.

Using a library (the production way)

For production code where you also need to verify the signature:

# pip install PyJWT
import jwt

# Decode WITHOUT verification (inspect only)
payload = jwt.decode(token, options={"verify_signature": False})

# Decode WITH verification (proper auth)
payload = jwt.decode(token, secret_key, algorithms=["HS256"])
// npm install jsonwebtoken
const jwt = require('jsonwebtoken');

// Decode without verification
const payload = jwt.decode(token);

// Verify and decode
const payload = jwt.verify(token, secretKey);

When is it safe to decode without verifying?

It's safe when:

  • You're debugging — you want to inspect what's in a token without setting up the full verification stack.
  • You're in a service that receives pre-verified tokens — e.g. an internal microservice behind an API gateway that already validated the token.
  • You need to read non-sensitive claims like exp (expiry) to decide whether to even attempt verification before making a network call.

It's not safe when:

  • You're using the decoded claims to make authorization decisions (access control, permissions, payment flows). Always verify the signature first in these cases.
  • The token came from an untrusted source. Anyone can construct a JWT with any payload — without signature verification, you can't know who issued it.

Reading the expiry claim

A common pattern is to check whether a token has expired before sending a request:

import time

payload = decode_jwt_payload(token)
exp = payload.get("exp")
if exp and exp < time.time():
    print("Token is expired — refresh before use")

The exp claim is a Unix timestamp (seconds since epoch).

Key takeaways

  • JWT payloads are Base64URL-encoded, not encrypted. Anyone can read them.
  • Decode by splitting on ., taking segment 1, and Base64URL-decoding it.
  • Only skip signature verification for debugging or pre-verified contexts.
  • For any authorization decision, always verify the signature with the correct key.