Skip to main content
Aarunya AppsAarunya Apps

How it works

  1. 1

    Paste your JWT

    Drop a token from your app, an API response, or a cookie. The header, payload, and signature are colour-coded immediately.

  2. 2

    Inspect claims and expiry

    See the decoded JSON header and payload, every claim in a table, and a live badge showing whether the token is valid or expired.

  3. 3

    Verify the signature (optional)

    For HMAC-SHA256 tokens, enter your secret to verify the signature locally using the Web Crypto API — your secret never leaves the tab.

🛡️ Verify zero uploads — open DevTools → Network tab

Open your browser's DevTools (F12), go to the Network tab, and use this tool. You will see zero outbound requests — all processing runs inside your browser sandbox via WebAssembly or pure JavaScript. Nothing you paste or upload is ever sent anywhere.

⚠️ Security considerations

🚫

Never paste production JWTs into any online tool

A JWT for a live user session carries real privileges. Even if the tool is client-side, build the habit of only debugging with test tokens or already-invalidated tokens. A compromised JWT lets an attacker act as the user until it expires.

🔑

HS256 secret strength determines your entire security posture

With HMAC-SHA256 (HS256), anyone who knows the secret can forge arbitrary tokens. Use a cryptographically random 256-bit secret (32 bytes from crypto.randomBytes). Never use a short or dictionary-based string — HS256 secrets are brute-forceable offline.

⏱️

Always set exp — JWTs without expiry are permanent credentials

A JWT with no exp claim never expires. If it leaks, it works forever. Short-lived tokens (15 minutes for access tokens, 7 days for refresh tokens) limit the blast radius of a token leak. Use a refresh token flow for long sessions.

🎯

Validate iss and aud in your verification code, not just the signature

A valid signature only proves the token wasn't tampered with. Your server must also check that iss matches your expected issuer and aud matches your service. Without these checks, a token from a different service with the same secret passes signature verification.

🗝️

Prefer RS256 (asymmetric) over HS256 for multi-service architectures

With RS256, only the auth server holds the private key; all other services verify with the public key. This eliminates the need to share a shared secret across services. HS256 requires every verifying service to know the secret — a larger attack surface.

Use cases

Debug auth flows during development

Paste the token your backend issues and immediately see which claims are present, whether the expiry is set correctly, and that the audience (aud) is right.

Verify JWT signature without a server

Confirm that a token was signed with your secret locally, without sending it through a third-party debugger like jwt.io.

Audit third-party API tokens

Inspect JWTs from external providers (Auth0, Supabase, Firebase, Clerk) to understand the included claims, scopes, and expiry windows.

Common mistakes

5 pitfalls to avoid

1Not validating the alg header

✗ Wrong: Accepting any algorithm the token claims in its header — allows the 'alg:none' attack where attacker strips the signature

✓ Fix: Always pin the expected algorithm in your verification code: jwt.verify(token, secret, { algorithms: ['HS256'] }). Reject tokens that claim alg:none.

2Storing JWTs in localStorage

✗ Wrong: Storing tokens in localStorage because it's easy — exposes them to XSS attacks from any script on the page

✓ Fix: Store JWTs in httpOnly cookies (not accessible to JavaScript). If localStorage is required, harden against XSS with a strict Content-Security-Policy.

3Using a weak or guessable HS256 secret

✗ Wrong: JWT_SECRET=mysecret or JWT_SECRET=password — brute-forceable offline in seconds with hashcat

✓ Fix: Generate a 256-bit random secret: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))". Store it in a secrets manager, not in .env files.

4Skipping audience (aud) validation

✗ Wrong: Only checking the signature — a token issued for service A is accepted by service B

✓ Fix: Set aud to the specific service identifier when issuing tokens, and verify it matches on every decode: jwt.verify(token, secret, { audience: 'my-api' }).

5Treating JWT verification as authentication

✗ Wrong: Accepting any valid JWT as proof of identity — doesn't account for revoked tokens or logged-out sessions

✓ Fix: For logout/revocation, maintain a token denylist (Redis set of jti values) and check it on every request, or use very short exp + refresh token rotation.

Code snippets

Copy-ready integration examples

Node.js — sign and verify with jsonwebtoken
js
import jwt from "jsonwebtoken" // npm install jsonwebtoken

const secret = process.env.JWT_SECRET! // must be 256-bit random

// Sign a token (access token, 15-minute expiry):
const token = jwt.sign(
  { sub: userId, role: "user", aud: "my-api" },
  secret,
  { algorithm: "HS256", expiresIn: "15m", issuer: "my-auth-service" }
)

// Verify — throws if invalid, expired, wrong alg, wrong aud:
try {
  const payload = jwt.verify(token, secret, {
    algorithms: ["HS256"],  // pin algorithm — never omit
    audience: "my-api",
    issuer: "my-auth-service",
  })
  console.log(payload.sub) // user ID
} catch (err) {
  // TokenExpiredError, JsonWebTokenError, NotBeforeError
  return Response.json({ error: "Unauthorized" }, { status: 401 })
}
Next.js App Router — JWT auth middleware
tsx
// middleware.ts
import { NextRequest, NextResponse } from "next/server"
import { jwtVerify } from "jose" // npm install jose

const secret = new TextEncoder().encode(process.env.JWT_SECRET)

export async function middleware(req: NextRequest) {
  const token = req.cookies.get("access_token")?.value
  if (!token) return NextResponse.redirect(new URL("/login", req.url))

  try {
    const { payload } = await jwtVerify(token, secret, {
      algorithms: ["HS256"],
      audience: "my-api",
      issuer: "my-auth-service",
    })
    // Forward user ID to route handlers via header:
    const res = NextResponse.next()
    res.headers.set("x-user-id", payload.sub as string)
    return res
  } catch {
    return NextResponse.redirect(new URL("/login", req.url))
  }
}

export const config = { matcher: ["/dashboard/:path*", "/api/user/:path*"] }
Python — verify with PyJWT
python
# pip install PyJWT cryptography
import jwt
import os

secret = os.environ["JWT_SECRET"]

# Sign:
token = jwt.encode(
    {"sub": user_id, "aud": "my-api", "exp": datetime.utcnow() + timedelta(minutes=15)},
    secret,
    algorithm="HS256"
)

# Verify — raises exceptions on failure:
try:
    payload = jwt.decode(
        token,
        secret,
        algorithms=["HS256"],  # pin — never pass algorithms=jwt.get_unverified_header(token)
        audience="my-api",
    )
    user_id = payload["sub"]
except jwt.ExpiredSignatureError:
    raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
    raise HTTPException(status_code=401, detail="Invalid token")
Go — verify with golang-jwt
go
// go get github.com/golang-jwt/jwt/v5
import (
    "github.com/golang-jwt/jwt/v5"
    "os"
)

secret := []byte(os.Getenv("JWT_SECRET"))

// Parse and verify:
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
    // Pin algorithm — reject tokens claiming a different alg:
    if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
    }
    return secret, nil
}, jwt.WithAudience("my-api"), jwt.WithIssuer("my-auth-service"))

if err != nil || !token.Valid {
    http.Error(w, "Unauthorized", http.StatusUnauthorized)
    return
}

claims := token.Claims.(jwt.MapClaims)
userID := claims["sub"].(string)

Frequently Asked Questions

Is it safe to paste my JWT here?

Yes. This tool decodes entirely in your browser using JavaScript — no data is sent to any server. Open DevTools → Network while using it and you'll see zero requests. That said, avoid pasting production tokens for live users; use test tokens for debugging.

What does the signature verification check?

For HMAC-SHA256 (HS256) signed tokens, the tool re-computes the expected signature from the header and payload using your secret, then compares it to the signature in the token. If they match, the token is authentic and hasn't been tampered with. RSA and ECDSA (RS256, ES256) require the public key — public-key verification is coming soon.

What are the most common JWT claims?

Standard claims: iss (issuer), sub (subject / user ID), aud (audience), exp (expiry timestamp), iat (issued at), nbf (not before), jti (unique token ID). Claims in blue in the table are standard RFC 7519 claims; others are custom claims added by your application.

Why is the expiry time showing the wrong timezone?

JWT exp, iat, and nbf values are Unix timestamps (seconds since epoch). This tool converts them to your browser's local timezone for display. The raw Unix timestamp in the table is always timezone-independent.

How is this different from jwt.io?

jwt.io sends your token to its servers to decode. This tool decodes entirely client-side — your token never leaves the browser tab. It also runs offline once cached, and includes an expiry status badge and claims table without requiring you to scroll.

Want unlimited access + saved history?

Pro is $6/month · 30-day money-back guarantee.

Embed or share this toollink · markdown · html · iframe
https://aarunyaapps.com/jwt-debugger
Powered by Aarunya Apps