DevToolBoxFREE
BlogAdvertise

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

14분by DevToolBox

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

JSON Web Token은 어디에나 있습니다 — 하지만 대부분의 구현에는 심각한 보안 결함이 있습니다. 이 가이드는 알고리즘 선택, 토큰 만료 및 순환 전략, 리프레시 토큰 패턴, 주요 JWT 공격에 대한 방어를 다룹니다.

JWT란 무엇이고 어떻게 작동하나요?

JWT는 점으로 구분된 세 부분으로 구성된 Base64URL 인코딩 문자열입니다: 헤더(알고리즘 + 유형), 페이로드(클레임), 서명.

알고리즘 선택: HS256 vs RS256 vs ES256

가장 흔한 JWT 보안 실수는 분산 시스템에서 HS256을 사용하는 것입니다. RS256과 ES256은 비대칭 키를 사용합니다.

// Anatomy of a JWT — Header.Payload.Signature

// 1. Header (Base64URL decoded)
const header = {
  alg: "RS256",   // Algorithm: RS256 (asymmetric) preferred over HS256 (symmetric)
  typ: "JWT"
};

// 2. Payload (Base64URL decoded) — the claims
const payload = {
  iss: "https://auth.example.com",         // Issuer
  sub: "user_01HXYZ123",                  // Subject (user ID)
  aud: "https://api.example.com",          // Audience (target service)
  exp: 1735689600,                         // Expiry: Unix timestamp (15 min from now)
  iat: 1735688700,                         // Issued at
  jti: "8f14e0f9-21af-4c1e-8f26-3b4a5c6d7e8f", // Unique token ID (for revocation)
  roles: ["user"],                         // Custom claims
  email: "alice@example.com"
};

// 3. Signature (server-side only)
// RS256: sign(base64url(header) + '.' + base64url(payload), PRIVATE_KEY)
// Verification: verify(token, PUBLIC_KEY) — other services only need the public key

// --- Generating a JWT with jsonwebtoken (Node.js) ---
import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
import { randomUUID } from 'crypto';

const privateKey = readFileSync('./private.pem');   // RSA private key
const publicKey  = readFileSync('./public.pem');    // RSA public key (shared with all services)

function issueAccessToken(userId: string, roles: string[]): string {
  return jwt.sign(
    {
      sub: userId,
      aud: 'https://api.example.com',
      roles,
      jti: randomUUID(),
    },
    privateKey,
    {
      algorithm: 'RS256',
      issuer: 'https://auth.example.com',
      expiresIn: '15m',  // Short-lived access token
    }
  );
}

function verifyAccessToken(token: string) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],           // CRITICAL: whitelist algorithms — never allow 'none'
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
  });
}

토큰 만료 및 순환 전략

단기 액세스 토큰(15분)과 장기 리프레시 토큰(7~30일)의 조합이 업계 표준입니다.

// Refresh Token Pattern — Stateless Access + Stateful Refresh

import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';

// ---- Token issuance ----
async function issueTokenPair(userId: string) {
  const accessToken = jwt.sign(
    { sub: userId, jti: randomUUID() },
    process.env.JWT_PRIVATE_KEY!,
    { algorithm: 'RS256', expiresIn: '15m' }
  );

  // Refresh token: random bytes stored in DB — NOT a JWT
  const refreshToken = randomBytes(64).toString('hex');
  const refreshExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days

  // Store hashed refresh token in DB (never store plaintext)
  await db.refreshTokens.create({
    tokenHash: sha256(refreshToken),
    userId,
    expiresAt: refreshExpiry,
    createdAt: new Date(),
  });

  return { accessToken, refreshToken, refreshExpiry };
}

// ---- Token refresh endpoint ----
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies; // httpOnly cookie
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  const stored = await db.refreshTokens.findOne({
    tokenHash: sha256(refreshToken),
    expiresAt: { >: new Date() }, // Not expired
    revokedAt: null,                  // Not revoked
  });

  if (!stored) return res.status(401).json({ error: 'Invalid or expired refresh token' });

  // Rotate: invalidate old, issue new pair (refresh token rotation)
  await db.refreshTokens.update(
    { id: stored.id },
    { revokedAt: new Date() }         // Revoke old refresh token
  );

  const newPair = await issueTokenPair(stored.userId);

  // Set new refresh token as httpOnly cookie
  res.cookie('refreshToken', newPair.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    expires: newPair.refreshExpiry,
    path: '/auth/refresh',            // Scope cookie to refresh endpoint only
  });

  return res.json({ accessToken: newPair.accessToken });
});

// ---- Logout: revoke refresh token ----
app.post('/auth/logout', async (req, res) => {
  const { refreshToken } = req.cookies;
  if (refreshToken) {
    await db.refreshTokens.update(
      { tokenHash: sha256(refreshToken) },
      { revokedAt: new Date() }
    );
  }
  res.clearCookie('refreshToken', { path: '/auth/refresh' });
  return res.json({ success: true });
});

일반적인 JWT 공격 및 방어

가장 위험한 JWT 취약점: 알고리즘 혼동, RS256→HS256 혼동, 약한 시크릿, 누락된 exp 검증.

// JWT Attack Defenses — Code Patterns

// ---- ATTACK 1: Algorithm confusion ("none" bypass) ----
// VULNERABLE: never use verify() without specifying allowed algorithms
const VULNERABLE = jwt.verify(token, secret); // attacker can set alg: "none"

// SAFE: always whitelist algorithms
const SAFE = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Never include 'none', 'HS256' if you're using RS256
});

// ---- ATTACK 2: RS256 > HS256 confusion ----
// Attacker changes alg from RS256 to HS256 and signs with the PUBLIC key as the secret
// Defense: always specify algorithm on verify — never infer from the token header
// VULNERABLE:
const header = JSON.parse(atob(token.split('.')[0]));
jwt.verify(token, publicKey, { algorithms: [header.alg] }); // attacker controls this!

// SAFE:
jwt.verify(token, publicKey, { algorithms: ['RS256'] }); // hardcoded

// ---- ATTACK 3: Weak HS256 secrets (brute-force) ----
// VULNERABLE: short or predictable secrets
const BAD_SECRET = 'secret';
const BAD_SECRET2 = 'mysupersecretkey';

// SAFE: 256-bit cryptographically random secret
import { randomBytes } from 'crypto';
const GOOD_SECRET = randomBytes(32).toString('hex'); // 256-bit
// Store in environment variable, rotate annually

// ---- ATTACK 4: Missing expiry validation ----
// Some older libraries skip exp check by default
// SAFE: always validate exp (jsonwebtoken does this by default)
try {
  const decoded = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
    clockTolerance: 30, // Allow 30 seconds clock skew max
    ignoreExpiration: false, // NEVER set this to true in production
  });
} catch (err) {
  if (err instanceof jwt.TokenExpiredError) {
    return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
  }
  return res.status(401).json({ error: 'Invalid token' });
}

// ---- Token blacklist (for early revocation via jti) ----
import { createClient } from 'redis';
const redis = createClient();

async function revokeToken(jti: string, exp: number) {
  const ttl = exp - Math.floor(Date.now() / 1000); // seconds until natural expiry
  if (ttl > 0) {
    await redis.set(`jwt:revoked:${jti}`, '1', { EX: ttl });
  }
}

async function isTokenRevoked(jti: string): Promise<boolean> {
  return (await redis.get(`jwt:revoked:${jti}`)) !== null;
}

// Middleware: check blacklist
app.use(async (req, res, next) => {
  const decoded = jwt.verify(req.headers.authorization?.split(' ')[1] ?? '', publicKey, {
    algorithms: ['RS256'],
  });
  if (await isTokenRevoked((decoded as jwt.JwtPayload).jti ?? '')) {
    return res.status(401).json({ error: 'Token revoked' });
  }
  next();
});

안전한 토큰 저장

액세스 토큰: 메모리에 저장. 리프레시 토큰: httpOnly, Secure, SameSite=Strict 쿠키에 저장.

자주 묻는 질문

JWT와 불투명 세션 토큰 중 어떤 것을 사용해야 하나요?

서버 사이드 렌더링 앱에는 불투명 세션 토큰을 사용하세요. 마이크로서비스나 API에는 JWT를 사용하세요.

JWT를 만료 전에 취소하려면 어떻게 해야 하나요?

해결책: 짧은 만료 시간(15분), Redis의 토큰 블랙리스트, 사용자 레코드의 토큰 버전.

모든 JWT에 포함되어야 할 클레임은?

필수: iss, sub, aud, exp, iat, jti. 수신 서비스에서 항상 iss와 aud를 검증하세요.

JWT 시크릿과 개인 키의 길이는?

HS256 시크릿: 최소 256비트. RS256: 최소 2048비트 RSA 키. ES256: 256비트 타원 곡선 키.

관련 도구

도움이 되었나요?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Partner Picks

Sponsor this article

Place your product next to this developer topic with tracked clicks.

Ask about article sponsorship

Related Articles

JWT 인증: 완벽 구현 가이드

JWT 인증을 처음부터 구현. 토큰 구조, 액세스 토큰과 리프레시 토큰, Node.js 구현, 클라이언트 측 관리, 보안 모범 사례, Next.js 미들웨어.

HTTP 헤더 완전 가이드: 요청, 응답 및 보안 헤더

HTTP 헤더 완전 가이드: 요청, 응답 및 보안 헤더에 대한 설명.

REST API 설계 가이드: 2026년 모범 사례

견고한 REST API 설계 종합 가이드: 리소스 명명, HTTP 메서드, 페이지네이션, 에러 처리, 인증, 캐싱, OpenAPI.

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

무료 온라인 JWT 디코더로 JWT 헤더, 페이로드, 클레임을 즉시 검사하세요. JWT 구조, 표준 클레임, JavaScript/Python/Go/Java 디코딩, 서명 알고리즘, 보안 모범 사례 포함.

This site uses cookies for analytics and to display ads. By continuing to browse, you agree. Privacy Policy