JWKS & JWT 검증
Source:
oauth/jwks.md· Live: https://docs.1pass.dev/oauth/jwks LLM-sanitized: internal links absolutized, VitePress containers → admonitions, line numbers in the Jump-to Index reference this rendered file (1-indexed).
📍 Jump-to Index
- L23-L46: ## JWKS 엔드포인트
- L47-L67: ## 키 Rotation
- L55-L67: ### 검증 키 선택 (kty 필터)
- L68-L81: ## Payload 스키마
- L82-L138: ## 언어별 검증 예시
- L139-L143: ## 만료 이후
- L144-L151: ## Revoke 즉시 확인이 필요하면
- L152-L163: ## id_token (OIDC)
JWKS & JWT 검증
logi는 Access Token으로 RS256 JWT를 발급합니다. 제휴사는 stateless로 서명을 검증하거나, revocation을 확인하려면 /oauth/userinfo 를 호출합니다.
JWKS 엔드포인트
GET /.well-known/jwks.json응답 예시:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "ffaa476406e8abec",
"n": "xqJbzP...",
"e": "AQAB"
}
]
}Cache-Control: public, max-age=3600 헤더가 붙어 있어 1시간 캐시 가능합니다.
키 Rotation
- 분기 1회 새 키를 발급 +
kid변경 - 구 키는 JWKS에 90일 grace period 동안 유지 (기존 발급 토큰 검증용)
- 신규 발급은 활성
kid로만 서명
제휴사 구현 시 JWKS 캐시를 1시간 ~ 1일 기준으로 refresh. 검증 실패 시 한 번 강제 refresh 후 재시도.
검증 키 선택 (kty 필터)
JWKS 에서 서명 검증 키를 고를 때는 kid 매칭만 하지 말고 키 타입까지 필터하세요:
kty == "RSA"— RSA 키만use ∈ {null, "sig"}— 서명용(또는 미지정)alg ∈ {null, "RS256"}— RS256(또는 미지정)- 위 조건을 통과한 키 중에서
kid매칭
이렇게 하면 향후 JWKS 에 EC 등 다른 키 타입이 혼입되더라도 RSA 서명 키를 정확히 선택합니다. 같은 kid 를 공유하는 non-RSA 키가 섞여도 RSA 서명 키를 우선 선택하므로 미래 키 타입 확장에 견고합니다. logi SDK v1.0.1 이 이 필터를 구현합니다.
참고: 아래 코드 예시의
jose/ PyJWTPyJWKClient/jwx같은 표준 라이브러리는 내부적으로 이 필터링을 수행하므로 별도 처리가 필요 없습니다. 직접 JWKS 를 파싱해 키를 선택하는 경우에만 위 필터를 수동 구현하세요.
Payload 스키마
{
"iss": "https://api.1pass.dev",
"sub": "42",
"aud": "logi_a1b2c3d4...",
"exp": 1734567890,
"iat": 1734566990,
"jti": "9d6f...-...-...",
"scope": "profile:basic email"
}언어별 검증 예시
Code examples by language/framework:
import { jwtVerify, createRemoteJWKSet } from "jose";
const jwks = createRemoteJWKSet(new URL("https://api.1pass.dev/.well-known/jwks.json"));
const { payload } = await jwtVerify(accessToken, jwks, {
issuer: "https://api.1pass.dev",
audience: "logi_a1b2c3d4...", // 내 앱 client_id
});
console.log(payload.sub);require "jwt"
require "open-uri"
jwks_raw = URI.open("https://api.1pass.dev/.well-known/jwks.json").read
jwks = JWT::JWK::Set.new(JSON.parse(jwks_raw))
payload, _header = JWT.decode(
access_token, nil, true,
algorithms: ["RS256"],
iss: "https://api.1pass.dev", verify_iss: true,
aud: ENV["LOGI_CLIENT_ID"], verify_aud: true,
jwks: jwks
)import jwt
import requests
jwks = jwt.PyJWKClient("https://api.1pass.dev/.well-known/jwks.json")
signing_key = jwks.get_signing_key_from_jwt(access_token)
payload = jwt.decode(
access_token, signing_key.key,
algorithms=["RS256"],
issuer="https://api.1pass.dev",
audience="logi_a1b2c3d4...",
)import "github.com/lestrrat-go/jwx/v2/jwk"
jwks, _ := jwk.Fetch(ctx, "https://api.1pass.dev/.well-known/jwks.json")
tok, err := jwt.Parse([]byte(accessToken),
jwt.WithKeySet(jwks),
jwt.WithIssuer("https://api.1pass.dev"),
jwt.WithAudience("logi_a1b2c3d4..."),
)만료 이후
exp지나면expired_token에러- Refresh Token으로 /oauth/token 재호출하여 새 AT 발급
Revoke 즉시 확인이 필요하면
JWT는 stateless라 jti가 revoke되었는지는 자체 검증만으로는 알 수 없습니다. 아래 중 하나:
- 옵트인 — 민감 엔드포인트만
/oauth/userinfo호출해 logi에서 재검증 (15분 만료 기준으로 대부분 충분) - Webhook —
token.revoked이벤트 구독 (logi → 제휴사).jwt_jti수신 시 로컬 블랙리스트에 추가 - Introspection —
/oauth/introspect호출로active상태를 직접 확인
id_token (OIDC)
openid scope 요청 시 /oauth/token 응답에 id_token이 포함됩니다. 포함 claim:
iss,sub,aud,exp,iat,nonce(요청 시)at_hash—base64url_nopad(SHA256(access_token 의 UTF-8 바이트)[처음 16바이트])= left 128 bits, padding 없는 base64url (OpenID Connect Core 1.0 §3.1.3.6). logi 서버가 발급하며, SDK v1.0.1 은 이를 present-only 로 검증합니다 — 토큰 응답에access_token이 함께 주어질 때만access_token ↔ id_token바인딩을 확인하고,access_token이 없으면(refresh-only 등) 이 검사를 건너뜁니다.
💡 Tip: Production issuer 모든 검증 코드의
iss/issuer비교값은https://api.1pass.dev입니다. 이 값은/.well-known/openid-configuration의issuer필드와 정확히 일치합니다 — RP 가 hardcode 하지 말고 discovery 문서에서 fetch 해 비교하는 것을 권장합니다.
검증 방법은 AT와 동일하되, 추가로 nonce(authorize 요청 시 보낸 값과 일치하는지) 와 at_hash(위 공식으로 access_token 에서 계산한 값이 클레임과 일치하는지, present-only) 를 검증하세요 (replay + token 바인딩 방어).