LabHub
はじめる
배우기 러닝패스 코스

セッションとトークン — ブラウザからメッシュまで

トークン失効 — introspection、revocation、拒否リスト

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

Keycloak 이 발급한 토큰을 RFC 7009 로 폐기하고 RFC 7662 introspection 으로 그 결과를 확인한 뒤, 리소스 서버에서 로컬 검증·introspection·jti 차단목록 세 방식이 폐기된 토큰에 어떻게 반응하는지 실제 요청으로 증명한다.

왜 중요한가

서명된 JWT 는 리소스 서버가 혼자 검증하므로, 발급자에서 세션을 끝내도 API 는 그 사실을 모르고 토큰은 만료까지 통과한다. RFC 7009 도 자기 완결 토큰의 즉시 폐기에는 별도 연동이 필요하고 대안은 짧은 수명이라고 적는다. 그래서 설계자는 세 가지 중에서 고른다 — 매번 발급자에게 묻는 introspection(즉시, 대신 왕복과 가용성 결합), 리소스 서버의 차단목록(즉시, 대신 상태 저장소), 짧은 TTL(단순, 대신 최대 수명만큼 늦음). 이 실습은 세 방식을 한 서버에 나란히 두고 폐기된 토큰 하나가 각각에서 200 인지 401 인지, 그리고 그 대가가 몇 밀리초인지를 직접 재게 한다.

단계

  1. Keycloak 이 꺼져 있으면 lab-start-keycloak 으로 켜고, /root/st/revoke/wait.shhttp://127.0.0.1:8080/realms/labhub 가 200 이 될 때까지 기다린다(최대 240초). 걸린 시간을 /root/st/revoke/ready.txtready_seconds=<정수> 로 적는다.
  2. /opt/fixtures/kc/realm-info.env 를 source 해서 공개 클라이언트 web-app 의 비밀번호 그랜트(사용자 dev1)로 토큰을 받고, 응답 본문 JSON 전체를 /root/st/revoke/token.json 에 저장한다.
  3. /root/st/revoke/introspect.sh <토큰> 이 introspection 엔드포인트를 api-svc 자격으로 불러 응답 JSON 을 그대로 출력하게 만든다. 비밀은 env 에서 읽는다. 새 토큰은 active=true, 가짜는 active=false.
  4. /root/st/revoke/revoke.sh <refresh 토큰> 이 RFC 7009 폐기 엔드포인트에 client_id=web-app, token, token_type_hint=refresh_token 을 보내게 만들고, 폐기 전후 결과를 /root/st/revoke/revoke.txtbefore=true, after=false, refresh_after=400 으로 적는다.
  5. /root/st/revoke/api.py 를 127.0.0.1:8307 에 띄운다(/health, JWKS 로만 보는 /local/me, 요청마다 introspection 하는 /introspect/me). /root/st/revoke/bench.py 로 두 경로를 10번 이상 재서 /root/st/revoke/latency.txtn, local_ms, introspect_ms, access_ttl, max_exposure_s 를 적는다.
  6. api.pyGET /guarded/me(차단목록 revoked:jti:<jti> 확인)와 POST /logout(jti 를 남은 수명만큼 차단목록에 올리고 refresh 를 Keycloak 에서 폐기)을 더하고 다시 띄운다.
  7. /root/st/revoke/e2e.sh 로 전체 흐름을 시험해 /root/st/revoke/e2e.out 에 일곱 줄을 남긴다.

참고

Keycloak 켜고 렐름 준비 기다리기

Keycloak 이 꺼져 있으면 lab-start-keycloak 으로 켜고, /root/st/revoke/wait.shhttp://127.0.0.1:8080/realms/labhub 가 200 이 될 때까지 기다린다(최대 240초). 걸린 시간을 /root/st/revoke/ready.txtready_seconds=<정수> 로 적는다.

Keycloak 은 JVM 이라 포트가 열린 뒤에도 렐름 적재까지 시간이 더 걸립니다. 고정 sleep 대신 while 루프로 상태 코드를 폴링하세요. 켜져 있는지는 lab-status 로 봅니다.

web-app 으로 dev1 토큰쌍 받기

/opt/fixtures/kc/realm-info.env 를 source 해서 공개 클라이언트 web-app 의 비밀번호 그랜트(grant_type=password, 사용자 dev1)로 토큰을 받고, 응답 본문 JSON 전체를 /root/st/revoke/token.json 에 저장한다. access_token·refresh_token·expires_in 이 있어야 한다.

토큰 엔드포인트는 env 의 KC_TOKEN_URL 입니다. 공개 클라이언트는 비밀 없이 client_id 만 보냅니다. 비밀번호에 특수문자가 있으니 --data-urlencode 를 쓰세요.

introspection 으로 active 확인

/root/st/revoke/introspect.sh <토큰> 이 RFC 7662 introspection 엔드포인트(/realms/labhub/protocol/openid-connect/token/introspect)를 기밀 클라이언트 api-svc 자격으로 불러 응답 JSON 을 그대로 출력하게 만든다. 비밀은 파일에 적지 말고 realm-info.env 에서 읽는다. 새 토큰은 active=true, 가짜 문자열은 active=false 가 나와야 한다.

introspection 은 호출자 인증이 필수입니다(토큰 스캐닝 방지). curl -u 로 Basic 인증을 주고 token 을 form 으로 POST 합니다. 비활성 토큰에는 active 말고 아무 정보도 오지 않는 것이 정상입니다.

RFC 7009 로 refresh 토큰 폐기

/root/st/revoke/revoke.sh <refresh 토큰> 이 RFC 7009 폐기 엔드포인트(/realms/labhub/protocol/openid-connect/revoke)에 client_id=web-app, token, token_type_hint=refresh_token 을 보내게 만든다. 새 토큰쌍으로 폐기 전후 access 토큰의 introspection 과 폐기 뒤 refresh 그랜트 결과를 /root/st/revoke/revoke.txtbefore=true, after=false, refresh_after=400 으로 적는다.

폐기 엔드포인트는 성공이든 무효 토큰이든 200 이라 응답 코드만으로는 먹혔는지 알 수 없습니다. 결과는 introspection 으로 확인하세요. 공개 클라이언트라도 client_id 를 빠뜨리면 거절됩니다.

로컬 검증 vs introspection — 폐기와 지연

/root/st/revoke/api.py 를 127.0.0.1:8307 에 띄운다. GET /health 는 200, GET /local/me 는 Bearer 토큰을 JWKS 로만 검증(서명·exp·iss·aud=lab-api)하고, GET /introspect/me 는 요청마다 introspection 의 active 로 판정한다(둘 다 성공 200, 실패 401). 그리고 /root/st/revoke/bench.py 로 두 경로를 같은 토큰으로 N 번 이상(10 이상) 재서 /root/st/revoke/latency.txtn=, local_ms=, introspect_ms=, access_ttl=(토큰의 exp-iat), max_exposure_s=(로컬 검증만 할 때 폐기된 토큰이 살 수 있는 최대 초)를 적는다.

PyJWT 의 PyJWKClient 가 JWKS 를 받아 캐시합니다. 로컬 경로는 발급자에게 묻지 않으니 폐기된 토큰도 만료까지 200 이 나옵니다 — 이 스텝에서는 그게 맞는 결과이고, 그 창의 길이가 곧 토큰 수명입니다. introspection 결과를 캐시하면 폐기가 늦게 반영됩니다.

jti 차단목록과 로그아웃

/root/st/revoke/api.pyGET /guarded/me(로컬 검증 뒤 Redis 키 revoked:jti:<jti> 가 있으면 401)와 POST /logout(Bearer 토큰의 jti 를 revoked:jti:<jti> 로 토큰의 남은 수명만큼 TTL 을 걸어 저장하고, form 본문의 refresh_token 을 RFC 7009 로 Keycloak 에서 폐기한 뒤 200)을 더하고 다시 띄운다. 로그아웃 뒤 같은 토큰으로 /guarded/me 가 401 이어야 한다.

TTL 은 exp 에서 지금 시각을 뺀 값입니다. 만료된 토큰은 서명 검증에서 어차피 거절되니 그 뒤로 기억할 이유가 없습니다. 차단목록만으로는 Keycloak 쪽 세션이 살아 있어 refresh 로 새 토큰을 받을 수 있으니 폐기 요청도 함께 보내세요. 코드를 고쳤으면 서버를 내렸다 다시 띄워야 합니다.

폐기 전략을 한 흐름으로 증명

/root/st/revoke/e2e.sh 로 새 토큰을 받아 introspection → /guarded/mePOST /logout/guarded/me·/local/me → introspection → refresh 그랜트 순으로 시험해 /root/st/revoke/e2e.outintrospect_before=true, guarded_before=200, logout=200, guarded_after=401, local_after=200, introspect_after=false, refresh_after=400 을 한 줄씩 남긴다.

앞 스텝의 introspect.sh 와 서버를 그대로 씁니다. 채점기는 파일만 보지 않고 e2e.sh 를 새로 한 번 더 돌려 같은 일곱 줄이 나오는지 봅니다. local_after 가 200 인 것이 짧은 TTL 이 메우는 창입니다.