JWK와 JWKS 제대로 이해하기
JWT를 검증하려면 서명을 확인할 공개 키가 필요한데요. 그런데 그 공개 키, 도대체 어떤 형식으로 표현하고 어떻게 주고받을까요?
전통적으로는 X.509 인증서나 PEM 같은 형식을 썼지만, JSON 기반인 JOSE 생태계에는 키를 위한 전용 형식이 따로 있습니다. 바로 JWK(JSON Web Key)입니다. 키 하나를 JWK로 표현하고, 여러 JWK를 묶으면 JWKS(JWK Set)가 되는데요. OAuth/OIDC에서 인가 서버가 공개 키를 배포할 때 쓰는 그 꾸러미가 바로 JWKS입니다.
이 글에서는 JWK가 무엇이고 어떤 필드로 이루어지는지부터, 그것들을 묶은 JWKS를 어떻게 배포하고 kid로 매칭하며 로테이션하는지까지 한 자리에 정리합니다.
JWT(Json Web Token)에서 토큰 구조 자체를, OAuth 2.0 엔드포인트 제대로 이해하기에서 자체 검증과 introspection의 트레이드오프를 다뤘으니, 이 글은 그 사이를 잇는 다리로 읽어주시면 좋겠습니다.
JWK란 무엇일까요?
JWK는 암호 키 하나를 JSON 객체로 표현하는 표준 형식입니다. JOSE 가족의 일원으로 RFC 7517에서 정의하는데요. RSA 공개 키든 타원 곡선 키든, 키의 종류와 파라미터를 약속된 JSON 필드에 담아 표현합니다.
PEM 같은 형식과 달리 JWK는 그 자체가 JSON이라, JSON으로 통신하는 웹 환경에서 다루기가 편합니다. HTTP 응답 본문에 그대로 실어 보내거나, 라이브러리에 객체째로 넘기기 좋죠.
JWK의 구조
RSA 공개 키 하나를 JWK로 표현하면 다음과 같습니다.
{
"kty": "RSA",
"use": "sig",
"kid": "2026-03-rsa",
"alg": "RS256",
"n": "xGOr-H7A-Efcz...long-base64...",
"e": "AQAB"
}
각 필드의 역할은 다음과 같습니다.
kty— Key Type.RSA,EC,OKP중 하나로, 키의 수학적 종류를 지정합니다use— 용도.sig(서명 검증)와enc(암호화) 둘 중 하나. JWT access token 검증에서는sig만 씁니다kid— Key ID. 이 키를 식별하는 문자열. JWT 헤더의kid와 매칭하는 데 쓰입니다. 뒤에서 자세히 다룹니다alg— 이 키가 쓰이는 서명 알고리즘.RS256,ES256,EdDSA등. JWT 헤더의alg와 일치해야 합니다- 키 본체 파라미터 —
kty에 따라 달라집니다. RSA는n(modulus)·e(exponent), EC는crv·x·y, OKP(Ed25519 등)는crv·x
키 본체 파라미터는 Base64url로 인코딩된 값으로 실려 있는데, 직접 디코딩해 다룰 일은 거의 없습니다. JWT 검증 라이브러리에 JWK를 그대로 넘기면 알아서 파싱해주니까요.
JWK Set, 키를 묶다
키가 하나뿐이라면 JWK 하나로 충분하지만, 실무에서는 키가 여럿일 때가 많습니다. 키를 교체하는 동안 옛 키와 새 키가 잠시 공존하기도 하고, 서명용과 다른 용도의 키를 함께 쓰기도 하죠. 이렇게 여러 JWK를 하나로 묶은 것이 JWKS(JWK Set)입니다.
JWKS는 최상위에 keys 배열 하나만 가진 단순한 JSON 문서이고, 배열의 각 원소가 JWK 하나입니다.
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "2026-03-rsa",
"alg": "RS256",
"n": "xGOr-H7A-Efcz...long-base64...",
"e": "AQAB"
},
{
"kty": "EC",
"use": "sig",
"kid": "2026-03-ec",
"alg": "ES256",
"crv": "P-256",
"x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
}
]
}
JWK와 JWKS 모두 같은 RFC 7517이 정의하며, OAuth는 이 JWKS를 jwks_uri라는 필드로 빌려다 씁니다.
왜 JWKS로 공개 키를 배포할까요?
자원 서버(Resource Server, 이하 RS)가 들어온 JWT를 검증하려면 두 가지가 필요합니다.
토큰에 붙은 서명을 만들 때 쓴 알고리즘과, 그 서명을 검증할 공개 키죠.
알고리즘은 JWT 헤더의 alg 필드로 바로 얻을 수 있는데, 공개 키는 그렇게 간단하지 않습니다.
가장 순진한 방법은 RS에 공개 키를 직접 박아두는 거예요. 하지만 인가 서버(Authorization Server, 이하 AS)가 키를 교체하는 순간 RS가 깨지고, 멀티 테넌트 환경이라면 키 관리가 지옥이 됩니다. 그렇다고 매 요청마다 AS에 “이 토큰 유효해?”를 묻는 introspection으로 돌아가면, 자체 검증의 이점인 네트워크 왕복 제거가 무의미해지고요.
JWKS가 이 사이의 절충입니다. AS가 공개 키 꾸러미를 한 URL에 공개하고, RS는 그걸 한 번 받아 캐시해두고 재사용하는 구조예요. 최초 한 번의 네트워크 비용만 지불하면 이후 수많은 요청을 RS 자체 프로세스 안에서 검증할 수 있습니다.
kid로 올바른 키 고르기
JWKS에 키가 여럿 들어있으면, 하나의 JWT가 들어왔을 때 어느 키로 검증해야 할까요?
이 매칭을 담당하는 게 kid(Key ID)입니다.
JWT의 헤더에도 kid가 실려오고, JWKS의 각 JWK에도 kid가 있어서, 두 값을 맞춰 같은 키를 골라냅니다.
{
"alg": "RS256",
"typ": "JWT",
"kid": "2026-03-rsa"
}
위 JWT가 들어오면 RS는 JWKS에서 kid가 "2026-03-rsa"인 JWK를 찾아 거기 있는 n·e로 서명을 검증합니다.
만약 JWKS 어디에도 해당 kid가 없다면 두 가지 가능성을 의심해야 해요.
- AS가 새 키로 교체했는데 RS의 JWKS 캐시가 갱신되지 않음
- 악의적으로 조작된 JWT
보통은 1번일 확률이 압도적으로 높습니다.
그래서 “JWKS에서 kid를 못 찾으면 한 번만 캐시를 강제 갱신해본다”가 표준 구현 패턴이에요.
kid 없는 JWT도 스펙상 허용되긴 하는데, 이 경우 RS는 JWKS의 모든 키에 대해 검증을 시도하거나 사전 약속된 단일 키를 써야 합니다.
실무에서는 AS에 kid 포함을 요구하는 게 기본이에요. 운영이 훨씬 단순해집니다.
jwks_uri: JWKS를 어디서 받나
RS가 JWKS 문서를 얻는 공식 경로는 AS Metadata의 jwks_uri 필드입니다.
AS는 자기 정보를 .well-known/oauth-authorization-server에 JSON으로 공개하고, 그 안의 jwks_uri가 JWKS 문서 주소를 가리켜요.
{
"issuer": "https://as.example.com",
"jwks_uri": "https://as.example.com/.well-known/jwks.json"
}
여기서 .well-known/jwks.json 경로는 관습일 뿐 스펙이 강제하지 않습니다.
공급자별로 실제 경로는 제각각이에요.
- Auth0:
https://{tenant}.auth0.com/.well-known/jwks.json - Google:
https://www.googleapis.com/oauth2/v3/certs - AWS Cognito:
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json - Microsoft Entra:
https://login.microsoftonline.com/{tenantId}/discovery/v2.0/keys
그래서 RS는 절대 .well-known/jwks.json을 하드코딩하지 말고, AS Metadata에서 jwks_uri를 읽어 쓰는 게 원칙입니다.
공급자를 바꾸거나 멀티 테넌트로 확장할 때 이 한 줄이 운명을 가릅니다.
자체 검증의 전체 흐름
지금까지 배운 조각을 실제 검증 순서로 이어붙이면 이렇습니다.
- RS가 기동 시점(또는 최초 요청 시점)에 AS Metadata를 조회해
jwks_uri를 얻는다 jwks_uri에 GET 요청을 보내 JWKS 문서를 받는다- JWKS 문서를 메모리에 캐시한다
- JWT가 들어오면 헤더의
kid를 꺼내 JWKS에서 같은kid를 가진 JWK를 찾는다 - 해당 JWK의 파라미터로 JWT 서명을 검증한다
- 서명이 유효하면 페이로드의
iss,aud,exp,nbf를 검증한다
중요한 건 4~6단계에 네트워크 호출이 없다는 점입니다. 캐시된 JWKS로 서명 검증을 수행하고, 페이로드에 박혀 있는 정보로 claim 검증을 수행해요. 바로 이 지점이 introspection 대비 자체 검증의 핵심 이점입니다.
import { createRemoteJWKSet, jwtVerify } from "jose";
// JWKS 자동 캐시 & 갱신
const JWKS = createRemoteJWKSet(
new URL("https://as.example.com/.well-known/jwks.json"),
);
// 서명·claim 검증
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://as.example.com",
audience: "https://api.example.com",
});
console.log(payload.sub, payload.exp);
createRemoteJWKSet이 내부적으로 JWKS를 페치·캐시·갱신까지 알아서 처리합니다.
kid가 캐시에 없으면 한 번 재페치를 시도하고, 그래도 없으면 에러를 내는 일반적인 구현이에요.
캐시와 키 로테이션
JWKS 운영의 절반은 캐시 관리에 달려 있습니다. 두 힘이 서로 반대 방향으로 작용하거든요.
- 오래 캐시할수록 좋다 → 네트워크 왕복과 AS 부하를 줄이기 위해
- 짧게 캐시할수록 좋다 → 키가 교체됐을 때 빨리 반영하기 위해
실무에서 자리 잡은 절충점은 다음과 같습니다.
- 기본 캐시 수명은 수 시간 단위 — 대부분의 IdP가 JWKS 응답에
Cache-Control: max-age=...헤더를 실어 보내고, 이 값을 따르는 게 안전합니다. AS가 공식적으로 광고하는 교체 주기와 맞춰져 있거든요 kid미스 시 강제 재페치 — 캐시된 JWKS에 없는kid를 만나면 TTL 만료 전이라도 한 번 다시 받아옵니다. 이게 AS의 키 교체를 빨리 따라잡는 핵심 장치예요- 쿨다운 필수 —
kid미스마다 매번 재페치하면 악의적인 JWT 폭풍이 DDoS 통로가 되니, 같은kid미스를 짧은 시간 반복하면 재페치를 건너뛰는 쿨다운을 둡니다
AS 쪽의 키 로테이션은 보통 다음 패턴을 따릅니다.
- 새 키를 생성하고 JWKS에 기존 키와 함께 게시한다 (두 키 공존 기간)
- 일정 시간 동안 여전히 기존 키로 서명한다
- 새 키로 서명을 전환한다
- 기존 키로 서명된 토큰의 수명이 모두 끝난 뒤에 JWKS에서 기존 키를 제거한다
2단계의 공존 기간이 핵심입니다.
RS가 캐시를 갱신하기 전에 새 키로 서명된 토큰이 먼저 들어오는 걸 막아주거든요.
AS가 이 기간 없이 바로 교체해버리면 RS들이 일제히 kid 미스를 겪고, AS에 JWKS 재요청 폭탄이 터집니다.
실무에서 자주 만나는 함정
몇 가지 전형적인 사고 패턴을 미리 알아두면 디버깅이 훨씬 빨라집니다.
.well-known/jwks.json하드코딩 — 경로는 공급자마다 다르다는 걸 앞서 봤습니다. 반드시 AS Metadata에서jwks_uri를 읽어 쓰세요alg고정 없이 검증 — JWT 라이브러리에alg를 지정하지 않으면alg: none공격이나 키 혼동 공격에 노출될 수 있습니다. AS가 허용한 알고리즘 목록으로 엄격히 제한하세요iss·aud검증 생략 — 서명만 맞으면 통과시키는 구현이 꽤 많습니다. 다른 테넌트·다른 audience용 토큰을 재사용하는 공격을 막으려면 반드시iss와aud까지 검증해야 합니다- JWKS 페치 타임아웃 미설정 — AS가 느려지면 RS의 검증 스레드가 줄줄이 블로킹됩니다. 짧은 타임아웃과 기존 캐시 유지 전략(
stale-while-revalidate스타일)을 함께 둬야 안전합니다 - 여러 테넌트의 JWKS를 한 캐시에 뒤섞기 — 멀티 테넌트 AS에서는
issuer마다 JWKS가 다릅니다. 캐시 키를issuer단위로 분리하지 않으면 교차 오염이 생길 수 있어요
마치며
지금까지 JWK가 키 하나를 JSON으로 표현하는 형식이라는 점부터, 그것들을 묶은 JWKS, 그리고 jwks_uri 배포와 kid 매칭, 캐시와 로테이션, 실무 함정까지 살펴봤습니다.
정리하면 JWK는 키 한 개, JWKS는 그 묶음이고, “공개 키를 공개 URL로 배포한다”는 이 단순한 아이디어가 OAuth 2.1 시대 자체 검증 아키텍처 전체를 떠받치고 있습니다.
JWK는 JOSE 레이어의 조각이라 OAuth와는 층위가 다르지만, jwks_uri라는 다리를 통해 OAuth Metadata와 연결됩니다.
OAuth 2.0 메타데이터와 엔드포인트 동적 발견에서 jwks_uri가 어떻게 발견 흐름에 끼어드는지, MCP Authentication에서 자체 검증을 전제로 한 최신 프로토콜이 이 메커니즘을 어떻게 쓰는지 확인해보세요.
키 형식의 정확한 명세는 RFC 7517 - JSON Web Key에서 확인할 수 있습니다.
This work is licensed under
CC BY 4.0