2026 . 오은

~~~~~~><>

JWK의 정체?!

2026.07.28

2026.07.28

JWT vs 세션, 그리고 JWK의 정체

TL;DR

"JWT가 죽은 게 아니라, JWT를 브라우저 세션 관리 도구로 쓰던 오용이 교정된 것." 다시 상기하자, 쿠키 vs JWT는 대립 축이 아님, 쿠키는 전송 수단, JWT는 토큰 포맷.

발단

better-auth, Supabase Auth 등 최근 인증 솔루션들이 JWT(JWK)를 지원하면서도 기본값은 쿠키+세션인 걸 보고 의문이 생겼다. 분명 몇 년 전엔 "JWT로 통일" 분위기였는데?

1. 인증 트렌드는 실제로 한 바퀴 돌았다

JWT 전성기 (~2015–2021)

SPA + REST API 분리 아키텍처의 유행과 정확히 겹친다. 프론트와 백이 다른 도메인, "stateless라 스케일링에 좋다"는 마케팅, 튜토리얼마다 복붙되던 localStorage 저장 패턴.

실무에서 누적된 문제

문제내용
폐기(revocation)로그아웃·강제 탈퇴·권한 변경을 즉시 반영하려면 결국 서버에 블랙리스트/세션 테이블 필요 → "stateless" 붕괴
refresh tokenaccess를 짧게 + refresh를 DB에 저장·검증 → 사실상 세션의 재발명. "You reinvented sessions with extra steps"
localStorageXSS에 그대로 노출. httpOnly 쿠키를 포기할 이유가 없음

회귀의 결정타 (2022–2025)

Next.js App Router/RSC, Remix 등 서버 렌더링 회귀로 프론트와 백이 같은 first-party 서버가 됨. 이 구조에서는 httpOnly 쿠키가 자연스럽고(CSRF는 SameSite로 대부분 해결), 클라이언트가 토큰을 직접 다룰 이유가 사라짐. Lucia 작자의 세션 기반 주장, better-auth의 쿠키+DB 세션 기본값이 이 맥락.

쿠키는 전송 수단, JWT는 토큰 포맷. 예로 Supabase는 내부적으로 여전히 JWT 기반(access token이 JWT)이지만, 저장/전송은 httpOnly 쿠키로 하는 하이브리드다.

현재 수렴된 컨센서스

  • first-party 웹앱 세션 → httpOnly 쿠키 (내용물은 opaque 세션 ID든 짧은 수명 서명 토큰이든)
  • 서비스 간 통신 / 서드파티 API / OIDC 연합 인증 → JWT가 여전히 표준, 대체재 없음
  • 하이브리드 → 짧은 수명(수분~1시간) JWT access + DB 추적 refresh/세션. 검증은 stateless의 이점, 폐기는 refresh 시점에 가능

2. JWK가 정확히 뭔가

JWK(JSON Web Key) = 암호화 키를 JSON으로 표현하는 포맷. 토큰도 프로토콜도 아니고, PEM 파일의 JSON 버전이라고 생각하면 된다.

{
  "kty": "RSA",
  "kid": "key-2026-01",
  "use": "sig",
  "alg": "RS256",
  "n": "0vx7agoebGcQ...",
  "e": "AQAB"
}
  • kty: 키 종류 (RSA, EC, OKP...)
  • kid: key ID — 여러 키 중 어느 것으로 검증할지 식별
  • n, e: RSA modulus / exponent, 수학적 키 재료 자체
  • use / alg: 서명용 여부, 알고리즘

계층 정리

약어정체
JWT토큰
JWS그 토큰의 서명 구조
JWK검증에 쓰는 키의 포맷
JWKS그 키들의 배포 방법 (.well-known/jwks.json)

검증 흐름

  1. JWT 헤더에서 kid를 읽는다
  2. JWKS 엔드포인트(예: https://<project>.supabase.co/auth/v1/.well-known/jwks.json)에서 같은 kid의 JWK를 찾는다 (보통 캐싱)
  3. 그 공개키로 서명을 검증한다

발급자가 새 키를 JWKS에 추가하고 새 토큰부터 새 키로 서명하면, 검증자들은 코드 변경 없이 kid 매칭만으로 따라온다. Google 로그인 검증 시 googleapis.com/oauth2/v3/certs를 가져오는 것도 같은 패턴.

3. HS256 → RS256: 기밀성이 아니라 무결성+증명

처음에 "대칭키+비대칭키 하이브리드 암호화(ex: TLS)랑 같은 거네"라고 생각했는데, 동기는 같고 메커니즘은 다르다.

  • 하이브리드 암호화: 비대칭으로 대칭키 교환 → 데이터는 빠른 대칭키로 암호화. 두 방식의 조합, 목적은 기밀성
  • JWT의 HS256 → RS256: 대칭 MAC을 비대칭 서명으로 대체. 목적은 무결성 + 발급자 증명 (payload는 base64라 누구나 읽을 수 있음. 숨기는 게 아니라 "위조되지 않았음"을 보장)

공유하는 뿌리는 "대칭키는 공유해야 해서 관리가 안 된다"는 키 배포 문제:

  • HS256: 검증자 전원이 secret 보유 → 하나만 털려도 토큰 위조 가능 (secret으로 서명까지 할 수 있으므로)
  • RS256: 공개키가 유출돼도 검증밖에 못 함 → 발급 권한은 발급자에게만

RS256 = RSASSA-PKCS1-v1_5 서명 + SHA-256 해시. ES256이면 RSA 대신 타원곡선(ECDSA P-256).

4. Supabase가 JWT를 쓸 수밖에 없는 이유: 분산 아키텍처

생각해보면 Supabase는 단일 서버가 아니라 별도 컴포넌트들의 묶음이다. 로컬에서 supabase start 하면 Docker 컨테이너가 대여섯 개 뜨는 이유.

Kong (API 게이트웨이) ── 모든 요청의 입구
 ├─ GoTrue        /auth/v1/*      인증 서버 (Go)
 ├─ PostgREST     /rest/v1/*      테이블 → REST API
 ├─ Realtime      /realtime/v1/*  WebSocket 구독 (Elixir)
 ├─ Storage API   /storage/v1/*
 └─ PostgreSQL    ── 모두가 공유하는 DB

요청마다 각 컴포넌트가 Auth 서버에 "이 세션 유효해?"라고 물어보면 병목 → 각자 토큰만 보고 로컬 검증해야 함 → JWT의 원래 용도.

Supabase도 원래는 HS256 공유 secret이 기본이었고, JWKS 기반 비대칭 검증은 비교적 최근에 추가·권장되기 시작했다고 한다..

GoTrue의 정체

  • Netlify가 만든 Go 기반 인증 마이크로서비스를 Supabase가 포크 → 현재는 supabase/auth 리포로 독자 발전, 공식 명칭은 Supabase Auth
  • auth 스키마는 GoTrue의 전용 저장소일 뿐, 행위의 주체가 아니다
  • 회원가입 시: GoTrue가 auth.users에 row 쓰기 → SMTP 인증 메일 발송 → 확인 클릭 받아 email_confirmed_at 채움 → JWT 서명·발급. OAuth 리다이렉트 왕복도 GoTrue 담당
  • Supabase가 auth 스키마 직접 수정을 경고하는 이유 = 남의 서비스 내부 상태이기 때문

RLS와 연결되는 지점

PostgREST는 요청의 JWT를 스스로 검증하고, payload의 sub(user ID)와 role을 PostgreSQL 세션 변수로 설정 → RLS 정책이 auth.uid()로 그 값을 읽어 행 단위 접근 제어. GoTrue는 발급에만 등장하고 검증은 분산 처리.

5. better-auth와의 대비

Supabase (GoTrue)better-auth
형태별도 인증 서버를 빌려 씀인증 로직을 내 서버에 심음
검증 주체각 컴포넌트가 JWT 분산 검증내 Next.js 프로세스가 DB 세션 조회
기본값JWT + JWKS쿠키 + DB 세션
인증 메일GoTrue가 SMTP로 직접 발송sendVerificationEmail 콜백에 Resend/SES 직접 연결
JWT 지원태생부터 JWT플러그인으로 JWKS 엔드포인트 노출 (외부 서비스 검증용)