DevToolBox무료
블로그

온라인 JWT 디코더: JSON Web Token 디코딩, 검사 & 디버그 (2026 가이드)

15분 읽기by DevToolBox

TL;DR

A JWT is a three-part token (Header.Payload.Signature) encoded in Base64URL. You can decode any JWT without a key to read its claims. Use the DevToolBox JWT Decoder to inspect tokens instantly. Always verify the signature server-side before trusting any claim for authorization. This guide covers JWT anatomy, decoding vs verifying, JavaScript (manual + jsonwebtoken + jose), Python (PyJWT), claims reference, algorithms, security best practices, debugging, and real-world patterns.

1. What Is a JWT? Anatomy of a JSON Web Token

A JSON Web Token (JWT), standardized in RFC 7519, is a compact, self-contained token for securely transmitting information as a JSON object. JWTs are used for authentication (OAuth 2.0, OpenID Connect), API authorization (Bearer tokens), and SSO. A JWT looks like three Base64URL-encoded strings separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJBbGljZSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The Three Parts

Header — Base64URL-decoded, the header is a JSON object specifying the algorithm and token type:

{
  "alg": "HS256",   // Algorithm: HS256, RS256, ES256, etc.
  "typ": "JWT"      // Token type
}

Payload — Contains the claims: assertions about the user and additional metadata:

{
  "sub": "user_123",           // Subject: who this token is about
  "name": "Alice",             // Custom claim
  "iat": 1700000000,           // Issued At (Unix timestamp)
  "exp": 1700003600,           // Expiration (1 hour later)
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com"
}

Signature — Created by the issuer using their secret/private key. For HS256:

HMACSHA256(
  base64url(header) + "." + base64url(payload),
  secret
)

JWT vs Session Tokens vs API Keys

PropertyJWTSession TokenAPI Key
StatelessYesNo (server stores session)No (server stores key)
Built-in ExpiryYes (exp claim)Server-side controlledUsually no
Self-containedYes (claims in token)No (data in DB)No (opaque)
Revocation SupportHard (needs blocklist)Easy (delete session)Easy (delete key)
Payload SizeMedium (grows with claims)Tiny (just an ID)Tiny
Use CaseAuth, SSO, microservicesTraditional web appsServer-to-server

2. Decoding vs Verifying — A Critical Distinction

Many developers confuse decoding and verifying a JWT. Understanding the difference is essential for building secure applications.

ActionWhat It DoesKey Required?Safe for AuthZ?
DecodeBase64URL-decodes header + payload to read JSONNoNEVER
VerifyValidates signature + checks exp, iss, aud claimsYesYES

When to decode only: Debugging, logging, displaying user info in a UI after the token has already been verified server-side, reading non-security-sensitive metadata.

When to verify: Any time you make an authorization decision based on a JWT claim. If you call jwt.decode() instead of jwt.verify() in your authorization middleware, an attacker can craft a token with arbitrary claims and bypass security entirely.

Security Warning: Never use decoded-only JWT data for authorization. Always verify the signature server-side with a trusted library before granting access.

3. Decode JWT Online with DevToolBox

The DevToolBox JWT Decoder is a free, client-side tool that lets you paste any JWT and instantly see:

  • Header — algorithm (alg), token type (typ), key ID (kid)
  • Payload — all claims with human-readable timestamps for exp, iat, nbf
  • Signature — raw signature bytes (for visual inspection)
  • Expiration status — whether the token is currently valid, expired, or not yet active

All processing happens in your browser — tokens are never sent to any server. This makes it safe for inspecting tokens in development and staging environments.

4. JavaScript — Decode a JWT Without a Library

Since a JWT is just Base64URL-encoded JSON, you can decode the header and payload with standard built-in functions — no library needed. Base64URL differs from Base64 by using - instead of + and _ instead of /, with no padding.

Browser (using atob)

function decodeJWT(token) {
  const [headerB64, payloadB64, signature] = token.split('.');

  function base64UrlDecode(str) {
    // Replace URL-safe chars, add padding
    const base64 = str
      .replace(/-/g, '+')
      .replace(/_/g, '/')
      .padEnd(str.length + (4 - (str.length % 4)) % 4, '=');
    return JSON.parse(atob(base64));
  }

  return {
    header: base64UrlDecode(headerB64),
    payload: base64UrlDecode(payloadB64),
    signature,
  };
}

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTcwMDAwMzYwMH0.abc';
const decoded = decodeJWT(token);
console.log(decoded.header);   // { alg: 'HS256', typ: 'JWT' }
console.log(decoded.payload);  // { sub: 'user_123', exp: 1700003600 }

// Check expiration
const isExpired = decoded.payload.exp * 1000 < Date.now();
console.log('Expired:', isExpired);

Node.js (using Buffer)

function decodeJWTNode(token) {
  const [headerB64, payloadB64, signature] = token.split('.');

  function base64UrlDecode(str) {
    // Buffer.from handles base64url directly in Node.js
    return JSON.parse(Buffer.from(str, 'base64url').toString('utf8'));
  }

  return {
    header: base64UrlDecode(headerB64),
    payload: base64UrlDecode(payloadB64),
    signature,
  };
}

// Usage
const { header, payload } = decodeJWTNode(token);
console.log(header);   // { alg: 'HS256', typ: 'JWT' }
console.log(payload);  // { sub: 'user_123', iat: 1700000000, exp: 1700003600 }

// Human-readable expiration
const expDate = new Date(payload.exp * 1000);
console.log('Expires:', expDate.toISOString());

5. JavaScript — The jsonwebtoken Library

The jsonwebtoken package is the most popular JWT library for Node.js with millions of weekly downloads. Install it with npm install jsonwebtoken.

const jwt = require('jsonwebtoken');
// ESM: import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET; // Never hardcode in production

// --- SIGN a token ---
const payload = {
  sub: 'user_123',
  name: 'Alice',
  role: 'admin',
};

const token = jwt.sign(payload, SECRET, {
  algorithm: 'HS256',
  expiresIn: '1h',      // or 3600 (seconds)
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com',
});

// --- VERIFY a token (validates signature + claims) ---
try {
  const verified = jwt.verify(token, SECRET, {
    algorithms: ['HS256'],      // Explicitly whitelist algorithms!
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
  });
  console.log('Valid token. Subject:', verified.sub);
} catch (err) {
  if (err.name === 'TokenExpiredError') {
    console.error('Token has expired at:', err.expiredAt);
  } else if (err.name === 'JsonWebTokenError') {
    console.error('Invalid token:', err.message);
  } else if (err.name === 'NotBeforeError') {
    console.error('Token not active yet:', err.date);
  }
}

// --- DECODE only (no verification — for inspection/logging) ---
const decoded = jwt.decode(token, { complete: true });
console.log(decoded.header);  // { alg: 'HS256', typ: 'JWT' }
console.log(decoded.payload); // { sub: 'user_123', iat: ..., exp: ... }
// WARNING: jwt.decode() is NOT safe for authorization!

Async Callback Style

// Promisified verify
const verifyAsync = (token, secret, options) =>
  new Promise((resolve, reject) => {
    jwt.verify(token, secret, options, (err, decoded) => {
      if (err) reject(err);
      else resolve(decoded);
    });
  });

// Usage with async/await
async function authenticate(token) {
  const payload = await verifyAsync(token, SECRET, {
    algorithms: ['HS256'],
  });
  return payload;
}

6. JavaScript — The jose Library (Modern, Edge-Compatible)

The jose library is a modern, dependency-free JWT library that runs in Node.js, browser, Deno, Cloudflare Workers, and other edge runtimes. It supports all JWA algorithms and is ideal for RS256/ES256 with JWKS. Install with npm install jose.

import { jwtVerify, SignJWT, createRemoteJWKSet, decodeJwt } from 'jose';

// --- SIGN with HS256 ---
const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const token = await new SignJWT({ sub: 'user_123', role: 'admin' })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setExpirationTime('1h')
  .setIssuer('https://auth.example.com')
  .setAudience('https://api.example.com')
  .sign(secret);

// --- VERIFY with HS256 ---
const { payload, protectedHeader } = await jwtVerify(token, secret, {
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com',
});
console.log(payload.sub); // 'user_123'

// --- VERIFY with RS256/ES256 using JWKS endpoint ---
// Ideal for OAuth 2.0 / OpenID Connect
const JWKS = createRemoteJWKSet(
  new URL('https://auth.example.com/.well-known/jwks.json')
);

const { payload: oauthPayload } = await jwtVerify(accessToken, JWKS, {
  issuer: 'https://auth.example.com',
  audience: 'your-client-id',
  algorithms: ['RS256'],
});

// --- DECODE only (no verification) ---
const claims = decodeJwt(token);
console.log(claims.exp); // Expiration timestamp

Next.js Middleware with jose

// middleware.ts (runs at the Edge)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from '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);
    // Attach user info to headers for downstream handlers
    const headers = new Headers(req.headers);
    headers.set('x-user-id', payload.sub as string);
    return NextResponse.next({ request: { headers } });
  } catch {
    return NextResponse.redirect(new URL('/login', req.url));
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

7. Python — PyJWT

PyJWT is the standard JWT library for Python. Install it with pip install PyJWT. For RS256/ES256, also install pip install cryptography.

import jwt
import os
from datetime import datetime, timedelta, timezone

SECRET = os.environ["JWT_SECRET"]

# --- ENCODE (sign) a token ---
payload = {
    "sub": "user_123",
    "name": "Alice",
    "iat": datetime.now(tz=timezone.utc),
    "exp": datetime.now(tz=timezone.utc) + timedelta(hours=1),
    "iss": "https://auth.example.com",
    "aud": "https://api.example.com",
}
token = jwt.encode(payload, SECRET, algorithm="HS256")
print(token)  # "eyJhbGci..."

# --- DECODE and VERIFY ---
try:
    decoded = jwt.decode(
        token,
        SECRET,
        algorithms=["HS256"],       # Explicitly whitelist!
        audience="https://api.example.com",
        issuer="https://auth.example.com",
    )
    print("Subject:", decoded["sub"])
except jwt.ExpiredSignatureError:
    print("Token has expired")
except jwt.InvalidAudienceError:
    print("Audience mismatch")
except jwt.InvalidIssuerError:
    print("Issuer mismatch")
except jwt.InvalidTokenError as e:
    print("Invalid token:", e)

# --- DECODE only (no verification) ---
# options={"verify_signature": False} disables sig check
unverified = jwt.decode(
    token,
    options={"verify_signature": False},
    algorithms=["HS256"],
)
print("Unverified payload:", unverified)
# WARNING: Only use this for inspection/debugging!

Python — RS256 with a Public Key

from cryptography.hazmat.primitives import serialization

# Load RSA public key (for verification only)
with open("public_key.pem", "rb") as f:
    public_key = serialization.load_pem_public_key(f.read())

decoded = jwt.decode(
    token,
    public_key,
    algorithms=["RS256"],
    audience="https://api.example.com",
)

# Load RSA private key (for signing)
with open("private_key.pem", "rb") as f:
    private_key = serialization.load_pem_private_key(f.read(), password=None)

token = jwt.encode(payload, private_key, algorithm="RS256")

8. JWT Claims Reference

The JWT specification defines a set of registered claim names with well-known meanings. All timestamps are Unix timestamps (seconds since epoch, not milliseconds).

ClaimFull NameDescriptionTypeRequired?
issIssuerWho issued the token (URL or identifier)StringRecommended
subSubjectWho the token is about (user ID)StringRecommended
audAudienceIntended recipient(s) of the tokenString/ArrayRecommended
expExpirationWhen the token expires (Unix timestamp)NumberStrongly recommended
iatIssued AtWhen the token was createdNumberRecommended
nbfNot BeforeToken not valid before this timeNumberOptional
jtiJWT IDUnique identifier (prevents replay attacks)StringOptional

Validating Claims Manually (JavaScript)

function validateClaims(payload, options = {}) {
  const now = Math.floor(Date.now() / 1000); // Current Unix timestamp

  // Check expiration (exp)
  if (payload.exp && payload.exp < now) {
    throw new Error(`Token expired at ${new Date(payload.exp * 1000).toISOString()}`);
  }

  // Check not-before (nbf)
  if (payload.nbf && payload.nbf > now) {
    throw new Error(`Token not yet valid until ${new Date(payload.nbf * 1000).toISOString()}`);
  }

  // Check issuer (iss)
  if (options.issuer && payload.iss !== options.issuer) {
    throw new Error(`Invalid issuer: expected ${options.issuer}, got ${payload.iss}`);
  }

  // Check audience (aud)
  if (options.audience) {
    const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
    if (!aud.includes(options.audience)) {
      throw new Error(`Invalid audience: ${payload.aud}`);
    }
  }

  return true;
}

9. JWT Signing Algorithms Compared

AlgorithmTypeKey TypeBest ForSecurity Level
HS256Symmetric (HMAC)Shared secretSingle service (same signer + verifier)Good (if secret is strong)
HS384Symmetric (HMAC)Shared secretSame as HS256, larger digestGood
HS512Symmetric (HMAC)Shared secretSame as HS256, largest digestGood
RS256Asymmetric (RSA)Private + Public keyMicroservices, OAuth 2.0, OIDCStrong
RS384Asymmetric (RSA)Private + Public keyHigher security RSAStrong
ES256Asymmetric (ECDSA)EC Private + Public keyMobile, IoT, performance-criticalVery Strong
EdDSAAsymmetric (Ed25519)Ed25519 key pairModern systems, small tokensVery Strong
noneNo signatureNoneNEVER use in productionNone (dangerous)

When to use HS256: When both the issuer and verifier are the same service and you can securely share a secret. Simple, fast, no key infrastructure needed.

When to use RS256/ES256: In distributed systems where multiple services need to verify tokens but only one service should sign them. The auth server holds the private key; all other services use the public key (often fetched from a JWKS endpoint).

The alg: none Attack: Some vulnerable JWT libraries accept tokens with alg: 'none' and no signature, treating them as valid. Always explicitly whitelist allowed algorithms and reject none. Never accept the algorithm from the token header without cross-checking it against your expected algorithm.

10. JWT Security Best Practices

  • Always verify the signature — never use decode() for authorization; always use verify().
  • Validate critical claims — always check exp, iss, and aud during verification.
  • Whitelist algorithms — explicitly specify allowed algorithms in your library, e.g., algorithms: ['HS256']. Never accept none.
  • Use HTTPS always — JWTs sent over HTTP can be intercepted. Always use TLS.
  • Short-lived access tokens — keep access token expiry to 15 minutes or 1 hour. Use refresh tokens for session persistence.
  • Rotate refresh tokens — issue a new refresh token on every use (refresh token rotation) and invalidate the old one.
  • Store tokens securely — prefer httpOnly, Secure, SameSite=Strict cookies over localStorage to prevent XSS access.
  • No sensitive data in payload — the payload is only Base64URL-encoded, not encrypted. Anyone who obtains the token can decode it. Never put passwords, SSNs, or PII.
  • Use strong secrets — for HS256, use at least 256 bits (32 bytes) of random entropy. Never use a dictionary word or short string.
  • Implement token revocation — maintain a blocklist (Redis with TTL) for cases where you need to invalidate tokens before expiry (logout, password change).
  • Clock skew tolerance — allow a small clock skew (up to 30 seconds) for nbf and exp validation when services have slightly different system clocks.

localStorage vs httpOnly Cookies — Tradeoffs

AspectlocalStoragehttpOnly Cookie
XSS protectionVulnerable (JS can read it)Protected (JS cannot read it)
CSRF protectionImmune (not auto-sent)Requires CSRF token or SameSite
CORS handlingEasyRequires credentials: include
PersistenceUntil clearedControlled by Max-Age/Expires
Mobile app useEasyRequires custom handling
RecommendationAvoid for sensitive tokensPreferred for web apps

11. Debugging Common JWT Errors

ErrorCauseFix
TokenExpiredErrorexp claim is in the pastRefresh the token using a refresh token or re-authenticate
JsonWebTokenError: invalid signatureToken tampered with, or wrong key usedCheck that the same key used to sign is used to verify
JsonWebTokenError: invalid algorithmalg in token header does not match expectedExplicitly set algorithms whitelist; check token header
NotBeforeErrornbf claim is in the futureAllow clock skew tolerance (e.g., clockTolerance: 30)
Invalid audienceaud claim does not match expected valueEnsure aud in token matches the audience you specify during verify
Malformed JWTToken does not have 3 dot-separated partsCheck token is not truncated; ensure Bearer prefix is stripped
Invalid issueriss claim does not match expected issuerCheck issuer URL matches exactly (trailing slash matters)
Clock skewServer clocks out of sync causing exp/nbf issuesSync clocks with NTP; add clockTolerance in verification options

Debug Checklist

// 1. Decode the token first to inspect claims
const decoded = jwt.decode(token, { complete: true });
console.log('Header:', decoded?.header);
console.log('Payload:', decoded?.payload);

// 2. Check expiration manually
const now = Math.floor(Date.now() / 1000);
console.log('Current time:', now);
console.log('Token exp:', decoded?.payload?.exp);
console.log('Expired:', decoded?.payload?.exp < now);

// 3. Check issuer and audience
console.log('iss:', decoded?.payload?.iss);
console.log('aud:', decoded?.payload?.aud);

// 4. Verify with explicit options to get specific errors
try {
  jwt.verify(token, SECRET, {
    algorithms: ['HS256'],
    // Comment these out temporarily to isolate issues:
    // issuer: 'https://auth.example.com',
    // audience: 'https://api.example.com',
  });
} catch (err) {
  console.error('Verification error name:', err.name);
  console.error('Verification error message:', err.message);
}

12. Real-World JWT Patterns

Express.js Auth Middleware

// middleware/auth.js
const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or invalid Authorization header' });
  }

  const token = authHeader.slice(7); // Remove "Bearer " prefix
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'],
      issuer: process.env.JWT_ISSUER,
      audience: process.env.JWT_AUDIENCE,
    });
    req.user = payload;
    next();
  } catch (err) {
    const status = err.name === 'TokenExpiredError' ? 401 : 403;
    return res.status(status).json({ error: err.message });
  }
}

// Role-based access control
function authorize(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

// Usage
app.get('/admin', authenticate, authorize('admin'), (req, res) => {
  res.json({ message: 'Admin area', user: req.user.sub });
});

Refresh Token Rotation Pattern

// POST /auth/refresh
async function refreshTokens(req, res) {
  const { refreshToken } = req.cookies; // httpOnly cookie
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  // 1. Verify the refresh token
  let payload;
  try {
    payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET, {
      algorithms: ['HS256'],
    });
  } catch {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }

  // 2. Check it exists in the database (rotation tracking)
  const stored = await db.refreshTokens.findOne({ token: refreshToken, userId: payload.sub });
  if (!stored) return res.status(401).json({ error: 'Refresh token reuse detected' });

  // 3. Delete the used refresh token (invalidate)
  await db.refreshTokens.deleteOne({ token: refreshToken });

  // 4. Issue new token pair
  const newAccessToken = jwt.sign(
    { sub: payload.sub, role: payload.role },
    process.env.JWT_SECRET,
    { expiresIn: '15m', algorithms: ['HS256'] }
  );
  const newRefreshToken = jwt.sign(
    { sub: payload.sub },
    process.env.REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  // 5. Store new refresh token
  await db.refreshTokens.insertOne({ token: newRefreshToken, userId: payload.sub });

  // 6. Set httpOnly cookie + return access token
  res.cookie('refreshToken', newRefreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000,
  });
  res.json({ accessToken: newAccessToken });
}

JWKS Endpoint for Microservices (jose)

// In any microservice — verify tokens issued by auth service
import { createRemoteJWKSet, jwtVerify } from 'jose';

// Fetch public keys from auth server's JWKS endpoint
// Keys are cached automatically by jose
const JWKS = createRemoteJWKSet(
  new URL(`${process.env.AUTH_SERVER}/.well-known/jwks.json`)
);

async function verifyToken(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: process.env.AUTH_SERVER,
    audience: process.env.SERVICE_NAME,
    algorithms: ['RS256'],
  });
  return payload;
}

// Each microservice can verify tokens independently
// without sharing any secret — only the auth server has the private key

Key Takeaways

  • A JWT has three Base64URL-encoded parts: Header, Payload, and Signature, joined by dots.
  • Decoding reads the claims without verifying authenticity. Verifying validates the cryptographic signature — always do this for authorization.
  • Use the DevToolBox JWT Decoder to inspect tokens instantly — all client-side, nothing sent to servers.
  • In JavaScript, use jsonwebtoken for Node.js or jose for edge runtimes. In Python, use PyJWT.
  • Standard claims: iss, sub, aud, exp, iat, nbf, jti — always validate them.
  • Use HS256 for single-service auth, RS256/ES256 for distributed/microservice architectures.
  • Never trust alg: none. Always whitelist algorithms explicitly. Never put sensitive data in the payload.
  • Store JWTs in httpOnly cookies (not localStorage) to protect against XSS attacks.
  • Use short-lived access tokens (15m–1h) with refresh token rotation for long-lived sessions.
𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

JWTJWT DecoderB64Base64 Encoder/Decoder#Hash Generator{ }JSON Formatter

Related Articles

JWT 작동 원리: JSON Web Token 완전 가이드

JWT 인증의 작동 방식, header, payload, signature 구조를 이해합니다.

JWT 보안 모범 사례: 알고리즘 선택, 만료, 순환 및 공격 방지

JWT 보안 모범 사례 2026: RS256 vs HS256, 토큰 만료 전략, 리프레시 토큰 순환, JWT 공격 방어.