jwt decode security authentication tokens

JWT의 이해: JSON Web Token 디코딩 및 생성 방법

JSON Web Token (JWT)에 대한 포괄적인 가이드. 안전한 인증 디버깅을 위해 토큰을 디코딩, 검증 및 생성하는 방법을 알아보세요.

소개: JSON Web Token이란 무엇인가?

JSON Web Token(JWT)은 RFC 7519로 표준화된 개방형 표준으로, JSON 객체 형태로 당사자 간에 정보를 안전하게 전송하기 위한 간결하고 자기 완결적인 방법을 정의합니다. 디지털 서명이 되어 있으므로 해당 정보를 검증하고 신뢰할 수 있습니다. JWT는 공유 비밀키(HMAC 알고리즘)나 공개키/개인키 쌍(RSA 또는 ECDSA)을 사용하여 서명할 수 있습니다.

JWT는 분산 시스템에서 흔히 발생하는 과제를 해결하기 위해 탄생했습니다. "서버가 데이터베이스에서 세션을 조회하지 않고도 클라이언트의 요청을 어떻게 신뢰할 수 있을까?" 그 답은 사용자의 신원과 권한을 토큰에 직접 인코딩하고 서명하여 클라이언트가 모든 요청에 함께 전달하는 것입니다.

오늘날 JWT는 현대 인증 및 권한 부여의 핵심 기술로 자리 잡았습니다:

  • 싱글 사인온(SSO): 한 번의 로그인으로 여러 서비스에 접근 가능.
  • API 인증: 모바일 앱과 SPA가 Bearer 토큰으로 API 호출 인증.
  • OAuth 2.0 / OpenID Connect: 액세스 토큰 및 ID 토큰으로 JWT 사용.
  • 마이크로서비스: 중앙 세션 저장소 없이 서비스 간 호출자 신원 검증.

JWT 구조: header.payload.signature

JWT는 점(.)으로 구분된 세 개의 Base64url 인코딩 부분으로 구성된 문자열입니다:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1부 — Header(헤더) 디코딩 결과:

{
  "alg": "HS256",
  "typ": "JWT"
}

헤더는 서명 알고리즘(alg)과 토큰 유형(typ)을 지정합니다.

2부 — Payload(페이로드) 디코딩 결과:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

페이로드에는 **클레임(Claims)**이 포함됩니다. 클레임은 엔티티(일반적으로 사용자)에 대한 설명과 메타데이터입니다.

3부 — Signature(서명) (HS256의 경우) 계산 방법:

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

서명은 토큰이 변조되지 않았음을 보장합니다. 중요: 페이로드는 Base64url 인코딩만 된 것으로, 암호화된 것이 아닙니다. JWT를 가진 누구나 모든 클레임을 읽을 수 있습니다.

서명 알고리즘: HS256, RS256, ES256

HS256 (HMAC-SHA256) — 대칭 알고리즘

서명과 검증에 동일한 공유 비밀키를 사용합니다. 구현이 간단하지만, 발급자와 검증자 모두 동일한 비밀키를 알고 있어야 합니다.

  • 적합한 경우: 단일 애플리케이션 또는 비밀키를 공유하는 긴밀하게 결합된 서비스.
  • 위험: 비밀키가 유출되면 모든 토큰을 위조할 수 있습니다.

RS256 (RSA-SHA256) — 비대칭 알고리즘

개인키로 서명하고 공개키로 검증합니다. 개인키는 비밀로 유지하고, 공개키는 JWKS 엔드포인트를 통해 자유롭게 공유할 수 있습니다.

  • 적합한 경우: 분산 시스템, 마이크로서비스, 여러 서비스가 토큰을 검증하는 환경.
  • 키 크기: 최소 2048비트 RSA 키 권장.

ES256 (ECDSA-SHA256) — 비대칭 알고리즘

P-256 곡선을 사용한 타원 곡선 디지털 서명 알고리즘입니다. RS256과 동등한 보안 수준에서 더 작은 서명을 생성합니다.

  • 적합한 경우: 성능에 민감하거나 대역폭이 제한된 환경.
알고리즘 유형 서명 크기 적합한 경우
HS256 대칭 공유 비밀키 32바이트 단일 서비스 앱
RS256 비대칭 RSA 키 쌍 256+ 바이트 분산 시스템
ES256 비대칭 EC 키 쌍 64바이트 고성능 API

JWT 클레임: 등록, 공개, 비공개

클레임은 페이로드에 인코딩된 엔티티(일반적으로 사용자)에 대한 설명과 메타데이터입니다.

등록된 클레임 (IANA 정의)

클레임 전체 이름 설명
iss Issuer 토큰을 발급한 주체 (예: "auth.example.com")
sub Subject 토큰이 가리키는 주체 (예: 사용자 ID)
aud Audience 토큰의 대상 수신자 (서비스 식별자)
exp Expiration 토큰이 무효화되는 Unix 타임스탬프
nbf Not Before 이 타임스탬프 이전에는 토큰을 사용해서는 안 됨
iat Issued At 토큰이 발급된 Unix 타임스탬프
jti JWT ID 리플레이 공격 방지를 위한 고유 식별자

공개 클레임

IANA JWT Claims Registry에 등록된 클레임. 예: email, name, picture, roles.

비공개 클레임

발급자와 소비자가 합의한 사용자 정의 클레임. 충돌을 방지하기 위해 네임스페이스를 사용하세요:

{
  "https://example.com/tenant_id": "acme-corp",
  "https://example.com/roles": ["admin", "editor"]
}

JWT 인증 흐름 작동 방식

  1. 사용자 로그인: 클라이언트가 인증 서버에 자격 증명(사용자 이름 + 비밀번호)을 전송.
  2. 토큰 발급: 서버가 자격 증명을 검증하고, 적절한 클레임을 포함한 JWT를 생성하여 서명 후 클라이언트에 반환.
  3. 토큰 저장: 클라이언트가 JWT를 메모리, localStorage, 또는 HttpOnly 쿠키에 저장.
  4. 인증된 요청: 보호된 리소스에 대한 각 요청마다 Authorization 헤더에 JWT 포함:
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
    
  5. 토큰 검증: 서버가 토큰을 추출하고, 서명을 검증하고, exp를 확인하여 클레임에 따라 접근 허용 또는 거부.
  6. 토큰 갱신: 액세스 토큰이 만료되면, 클라이언트는 장기 유효 리프레시 토큰을 사용하여 재인증 없이 새 액세스 토큰 획득.
클라이언트         인증 서버            리소스 서버
  |                    |                     |
  |-- POST /login ---->|                     |
  |<-- 200 + JWT ------|                     |
  |                    |                     |
  |-- GET /api (Authorization: Bearer JWT) ->|
  |                    |    JWT 검증          |
  |<----------- 200 + 데이터 ---------------->|

보안 고려 사항

페이로드는 암호화되지 않습니다

Base64url 인코딩은 암호화가 아닙니다. JWT를 가진 누구나 즉시 모든 클레임을 디코딩하고 읽을 수 있습니다. JWT 페이로드에 절대 포함하지 말 것:

  • 비밀번호 또는 비밀번호 해시
  • 신용카드 번호 또는 금융 데이터
  • 필요 이상의 민감한 개인정보(PII)
  • 개인키 또는 내부 시크릿

반드시 서명을 검증하세요

// Node.js — jsonwebtoken 라이브러리
const jwt = require("jsonwebtoken");

try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  console.log(decoded.sub); // 신뢰할 수 있는 사용자 ID
} catch (err) {
  res.status(401).json({ error: "유효하지 않거나 만료된 토큰입니다" });
}

짧은 만료 시간 설정

액세스 토큰의 유효 기간은 짧게 설정하세요(15분 ~ 1시간). 장기 세션에는 리프레시 토큰을 사용하여 토큰 도용 시 피해를 최소화합니다.

항상 HTTPS 사용

암호화되지 않은 연결로는 토큰을 전송하지 마세요. 가로채인 토큰은 훔쳐진 비밀번호와 다름없습니다.

일반적인 취약점

1. 알고리즘 혼동 공격 (CVE-2015-9235)

공격자가 헤더의 alg 필드를 RS256에서 HS256으로 변경하고, 서버의 공개키를 HMAC 시크릿으로 사용하여 토큰에 서명합니다. 부주의한 서버는 이것을 수락합니다.

대응책: 서버 측에서 예상 알고리즘을 명시적으로 지정하세요:

jwt.verify(token, publicKey, { algorithms: ["RS256"] });

2. "none" 알고리즘 공격

서버가 alg: "none"을 허용하면, 공격자는 서명 없는 토큰을 위조할 수 있습니다.

대응책: alg: "none" 토큰을 명시적으로 거부하세요. 최신 라이브러리는 이 문제를 수정했지만, 구버전은 여전히 취약할 수 있습니다.

3. 약한 시크릿 (HS256)

짧거나 추측하기 쉬운 HMAC 시크릿은 캡처된 토큰으로 hashcat 같은 도구를 이용해 오프라인 무차별 대입 공격을 받을 수 있습니다.

대응책: 최소 256비트의 암호학적으로 안전한 난수 시크릿을 사용하세요:

openssl rand -hex 32

4. 클레임 검증 누락

iss, aud, nbf를 검증하지 않으면 서비스 간 토큰 재사용이나 유효 기간 이전의 토큰 사용을 허용하게 됩니다.

5. 안전하지 않은 토큰 저장

localStorage는 페이지의 모든 JavaScript에서 접근 가능하여 XSS 공격에 취약합니다. 민감한 토큰에는 HttpOnly 쿠키를 사용하세요.

JWT vs 세션 토큰: 무상태 vs 유상태

특성 JWT (무상태) 세션 토큰 (유상태)
저장 위치 클라이언트 측 서버 측 (DB 또는 캐시)
확장성 우수 — 서버 조회 불필요 공유 세션 저장소 필요
취소 어려움 — exp까지 유효 간단 — 저장소에서 삭제
토큰 크기 요청당 더 큰 데이터 작은 불투명 토큰, 데이터는 서버에
마이크로서비스 이상적 — 독립적으로 검증 가능 공유 세션 인프라 필요
페이로드 가시성 토큰을 가진 누구나 읽을 수 있음 데이터는 서버 측에만 저장

JWT를 선택할 때: 분산 시스템, 무상태 REST API, 또는 모바일 앱 구축 시. 세션을 선택할 때: 즉각적인 취소가 필요한 경우 (예: "모든 기기에서 로그아웃").

취소의 어려움

JWT는 자기 완결적이므로 추가 인프라 없이는 만료 전에 토큰을 취소할 수 없습니다:

  • 짧은 만료 + 리프레시 토큰: 액세스 토큰은 빨리 만료되게 하고, DB에서 리프레시 토큰을 취소.
  • 토큰 차단 목록: Redis 같은 빠른 저장소에 취소된 jti 값 관리 (일부 유상태성 재도입).
  • 시크릿 교체: 서명 시크릿을 교체하여 모든 토큰 무효화 (모든 사용자에게 영향).

OAuth 2.0과 OpenID Connect

OAuth 2.0

OAuth 2.0은 권한 부여 프레임워크로, 특정 토큰 형식을 강제하지 않습니다. 하지만 JWT는 자기 완결적이고 권한 범위를 포함할 수 있어 OAuth 액세스 토큰으로 널리 사용됩니다:

{
  "iss": "https://auth.example.com",
  "sub": "user_123",
  "aud": "https://api.example.com",
  "scope": "read:profile write:posts",
  "exp": 1893456000
}

OpenID Connect (OIDC)

OpenID Connect는 OAuth 2.0 위에 구축된 신원 레이어로, ID 토큰을 도입합니다. ID 토큰은 항상 JWT이며, 인증된 사용자에 대한 신원 클레임을 포함합니다:

{
  "iss": "https://accounts.google.com",
  "sub": "110169484474386276334",
  "aud": "my-client-id.apps.googleusercontent.com",
  "email": "[email protected]",
  "name": "Jane Doe",
  "exp": 1893456000,
  "iat": 1893452400
}

OIDC는 JWKS 엔드포인트(/.well-known/jwks.json)도 정의하는데, 여기서 인증 서버가 공개키를 게시합니다. 모든 서비스는 사전 키 교환 없이 이 키를 가져와 토큰을 검증할 수 있습니다.

모범 사례

  1. 공개 시스템에는 비대칭 알고리즘(RS256/ES256) 사용 — 서명 개인키를 보호하면서 누구나 공개키로 검증 가능하게 합니다.
  2. 액세스 토큰에 짧은 exp 설정 — 15분이 일반적으로 안전한 기본값입니다.
  3. 리프레시 토큰 항상 교체 — 사용할 때마다 새 토큰을 발급하고 이전 토큰을 무효화합니다.
  4. 모든 관련 클레임 검증 — 최소한 iss, aud, exp, nbf를 확인하세요.
  5. 예상 알고리즘 명시적 지정 — 토큰 헤더의 alg 필드에만 의존하지 마세요.
  6. 페이로드에 민감한 데이터 저장 금지 — 인코딩이지 암호화가 아닙니다.
  7. 항상 HTTPS 사용 — 암호화되지 않은 연결로 토큰을 전송하지 마세요.
  8. 토큰 안전하게 저장 — localStorage보다 HttpOnly, Secure, SameSite=Strict 쿠키를 우선하세요.
  9. 토큰 취소 전략 구현 — 차단 목록이나 단기 토큰 + 리프레시 토큰 교체를 사용하세요.
  10. JWT 라이브러리 최신 상태 유지 — 새 버전에서 보안 취약점이 수정됩니다.

자주 묻는 질문 (FAQ)

Q: JWT 페이로드를 복호화할 수 있나요? A: JWT 페이로드는 Base64url 인코딩된 것으로, 암호화가 아닙니다 — 키 없이도 누구나 읽을 수 있습니다. 기밀성이 필요하다면 페이로드를 암호화하는 JWE(JSON Web Encryption, RFC 7516)를 사용하세요.

Q: decodeverify의 차이는 무엇인가요? A: decode는 단순히 각 부분을 Base64url 디코딩하여 JSON을 반환하며 보안 검사를 수행하지 않습니다. verify는 키로 서명을 검증하고 exp 같은 시간 기반 클레임도 확인합니다. 프로덕션에서는 반드시 verify를 사용하세요.

Q: 토큰 갱신을 안전하게 처리하려면? A: 단기 액세스 토큰(1560분)과 장기 리프레시 토큰(며칠몇 주)을 발급하세요. 리프레시 토큰은 서버 측에 저장하여 취소할 수 있게 합니다. 액세스 토큰이 만료되면 클라이언트는 전용 엔드포인트에 리프레시 토큰을 보내 새 액세스 토큰을 받습니다.

Q: JWT를 인증과 권한 부여 모두에 사용할 수 있나요? A: 네. 동일한 토큰에 신원 클레임(누구인지)과 권한 클레임(무엇을 할 수 있는지, 예: roles, scope)을 포함할 수 있습니다. 페이로드 크기를 적절히 유지하세요.

Q: JWT 시크릿이 유출되면 어떻게 하나요? A: 공격자가 모든 사용자의 유효한 토큰을 위조할 수 있습니다. 즉시 시크릿을 교체하고(기존 모든 토큰이 무효화됨), 유출 원인을 조사하고, 모든 사용자에게 재인증을 요구하세요.

Q: HS256과 RS256 중 어떤 것을 사용해야 하나요? A: 여러 서비스가 토큰을 검증하거나 검증자를 완전히 신뢰할 수 없을 때는 RS256(또는 ES256)을 사용하세요. 동일한 서비스가 발급과 검증을 모두 담당하고 시크릿을 엄격하게 관리할 수 있는 경우에만 HS256을 사용하세요.

Q: JWT의 최대 크기는 얼마인가요? A: HTTP 헤더 크기 제한과 성능 문제를 피하기 위해 4KB 미만으로 유지하세요. 큰 데이터 구조를 페이로드에 포함하지 말고 ID로 참조하세요.