LabHub
배우기 러닝패스 코스

Keycloakと企業認証

無効化という難しい問題

LabHub 에서 이어서 보기

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

한 줄 요약

JWT 는 발급 후 서버가 관여하지 않기 때문에 빠릅니다. 그리고 같은 이유로 취소하기 어렵습니다. 이 거래를 이해하고 수명으로 관리하는 것이 실무입니다.

概念マップ: 회전・블랙리스트 없이 15분 이내의 무효화를 얻습니다.・액세스 토큰 수명은 항상 유휴 시간보다 짧게・백채널 로그아웃

왜 이게 필요했나

"로그아웃했는데 토큰이 아직 유효해요" 는 JWT 를 쓰는 팀이 반드시 만나는 문제입니다. 세션 기반 인증에서는 서버가 세션 저장소에서 지우면 끝이었습니다. JWT 는 서버에 상태가 없으므로 지울 것이 없습니다.

해결책으로 블랙리스트를 떠올리게 됩니다. 취소된 토큰 ID 를 저장소에 넣고 매 요청 확인하는 것입니다. 그런데 이렇게 하면 매 요청 저장소 조회가 생기고, 무상태의 이점이 사라집니다. 세션 방식으로 돌아간 것과 크게 다르지 않습니다.

수명으로 관리한다

실무의 표준 답은 두 토큰의 수명을 다르게 두는 것입니다.

액세스 토큰 리프레시 토큰
수명 5~15분 며칠~몇 주
검증 서명만 확인(무상태) 인증 서버가 상태를 가짐
즉시 취소 불가능 가능
어디에 두나 메모리 httpOnly 쿠키 또는 안전한 저장소

액세스 토큰을 짧게 두면 탈취되어도 그 시간 뒤에는 무용지물이고, 권한을 회수해도 그 시간 안에 반영됩니다. 리프레시 토큰은 길게 두되 회전 시킵니다. 갱신할 때마다 새것을 주고 옛것을 무효화합니다.

이 조합의 효과가 중요합니다. 로그아웃하면 리프레시 토큰을 취소합니다. 액세스 토큰은 남아 있지만 최대 15분 뒤 만료되고, 그 뒤로는 갱신이 안 되므로 세션이 실질적으로 끝납니다. 블랙리스트 없이 15분 이내의 무효화를 얻습니다.

회전에는 부수 효과가 하나 더 있습니다. 옛 리프레시 토큰이 다시 쓰이면 그것은 탈취 신호입니다 — 정상 클라이언트는 새것을 갖고 있으니까요. 이때 그 사용자의 토큰 계열 전체를 무효화하는 것이 재사용 탐지(reuse detection)입니다.

정상:  RT1 → (갱신) → RT2 → (갱신) → RT3
탈취:  RT1 → (갱신) → RT2
        └── 공격자가 RT1 을 다시 사용 → 서버가 계열 전체를 끊는다

정말로 즉시 끊어야 하는 경우 — 계정 도용 신고, 관리자 강제 로그아웃 — 만 예외적으로 블랙리스트를 씁니다. 모든 토큰이 아니라 그 사용자의 토큰만 대상이므로 저장소 부담이 작습니다. 더 가벼운 방법은 사용자마다 token_not_before 시각을 하나 두고, 그보다 앞서 발급된 토큰을 전부 거부하는 것입니다. 저장소에 사용자당 값 하나면 됩니다.

Keycloak 의 네 가지 수명 설정

Keycloak 에는 여러 수명 설정이 있어 헷갈립니다. 각각 다른 것을 제어합니다.

설정 무엇을 제어하나 흔한 값
Access Token Lifespan 액세스 토큰이 유효한 시간 5~15분
SSO Session Idle 활동이 없으면 세션이 끝나는 시간 30분
SSO Session Max 활동이 있어도 강제로 끊는 시간 10시간
Offline Session Idle 오프라인 토큰의 유휴 한도 30일

유휴 시간과 최대 시간의 차이가 핵심입니다. 유휴는 "손을 떼면 끊긴다", 최대는 "계속 쓰고 있어도 언젠가 끊긴다" 입니다. 은행처럼 규제가 있는 곳에서는 최대 시간을 짧게 둡니다. 반대로 사내 도구는 최대 시간을 길게 두어 하루에 한 번만 로그인하게 합니다.

두 값의 관계를 잘못 잡으면 이상한 현상이 생깁니다. Access Token Lifespan 이 SSO Session Idle 보다 길면, 세션은 이미 끝났는데 액세스 토큰은 아직 유효한 구간이 생깁니다. 액세스 토큰 수명은 항상 유휴 시간보다 짧게 둡니다.

동시 로그인 제한도 자주 요구됩니다. Keycloak 은 세션 수 제한 기능을 제공하지만, 애플리케이션 차원에서 처리하려면 사용자별 세션 목록을 별도로 관리해야 합니다.

백채널 로그아웃과 BFF

여러 애플리케이션이 같은 인증 서버를 쓰면, 한 곳에서 로그아웃했을 때 다른 곳도 끊어야 합니다. OIDC 의 백채널 로그아웃 은 인증 서버가 각 애플리케이션의 등록된 주소로 로그아웃 토큰을 직접 보내는 방식입니다. 브라우저를 거치지 않으므로 탭이 닫혀 있어도 동작합니다. 받는 쪽은 그 토큰의 서명과 sid 를 확인하고 해당 세션을 지웁니다.

BFF(Backend for Frontend) 패턴도 세션 관리 관점에서 유리합니다. 토큰을 백엔드가 갖고 브라우저에는 세션 쿠키만 주면, 로그아웃 시 그 세션을 지우는 것으로 즉시 무효화가 됩니다. XSS 로 토큰이 털리는 경로도 사라집니다. 대신 백엔드가 상태를 갖게 되므로 수평 확장 시 세션 저장소가 필요합니다.

토큰을 어디에 두는가

수명 설계만큼 중요한 것이 저장 위치입니다. 잘못 두면 수명을 아무리 짧게 해도 소용이 없습니다.

위치 XSS 에 안전 CSRF 에 안전 새로고침 후 유지 평가
localStorage ✗ 스크립트가 읽는다 가장 흔하고 가장 위험하다
sessionStorage 탭 단위 나을 것이 없다
자바스크립트 변수 △ 같은 컨텍스트면 읽힌다 리프레시와 함께 쓰면 실용적
httpOnly 쿠키 ✓ 스크립트가 못 읽는다 ✗ SameSite 필요 리프레시 토큰의 자리

실무의 조합은 이렇습니다. 액세스 토큰은 메모리에, 리프레시 토큰은 httpOnly + Secure + SameSite=Lax 쿠키에. 새로고침하면 메모리의 액세스 토큰은 사라지지만 쿠키의 리프레시 토큰으로 조용히 다시 받습니다.

SameSite=Strict 는 외부 링크로 들어올 때 쿠키가 안 붙어 로그인이 풀린 것처럼 보입니다. 대개 Lax 가 맞고, 결제처럼 민감한 흐름만 Strict 로 좁힙니다.

시계가 어긋나면 벌어지는 일

JWT 검증은 expnbf 를 서버의 시계로 봅니다. 인증 서버와 자원 서버의 시계가 몇 초만 어긋나도 "방금 받은 토큰이 아직 유효하지 않다"(nbf 위반) 는 오류가 간헐적으로 납니다. 재현이 어려워 오래 헤매는 종류의 버그입니다.

검증 라이브러리는 대개 30~60초의 여유(clock skew)를 허용합니다. 이 값을 0 으로 줄이지 마세요. 대신 모든 노드에 NTP 를 맞춥니다.

현장에서 만나는 모습

토큰 수명 설정은 화면에서 몇 개의 숫자일 뿐인데, 그 숫자가 만드는 증상은 인증과 상관없어 보이는 형태로 나타난다.

"가끔 로그아웃돼요" 는 대개 세션 유휴 시간이다. 액세스 토큰은 갱신되는데 SSO 세션의 유휴 시간이 짧으면, 잠깐 자리를 비운 사용자가 돌아왔을 때 갱신이 거절된다. 사용자는 "몇 분 만에 풀렸다" 고 말하고, 로그에는 정상적인 만료로 남는다.

"로그아웃했는데 다른 탭은 살아 있어요" 는 백채널 로그아웃이 안 붙은 것이다. 프런트채널만으로는 열려 있는 다른 클라이언트가 그 사실을 모른다. 세션 ID 를 저장해 두고 로그아웃 통지를 받아 지우는 자리가 없으면, 로그아웃이 그 브라우저 탭에서만 일어난다.

"배포 직후에 전부 로그아웃돼요" 는 서명 열쇠가 바뀐 것이다. 열쇠를 돌릴 때는 옛 열쇠를 검증용으로 얼마간 남겨 두어야 한다. 공개 열쇠 목록(JWKS)을 캐시하는 쪽의 갱신 주기도 함께 본다.

"어떤 사용자만 안 돼요" 는 대개 토큰이 커진 것이다. 그룹이나 역할이 많은 계정은 토큰이 커지고, 그것을 쿠키에 담으면 헤더 크기 상한에 걸려 프록시가 431 이나 400 을 돌려준다. 증상이 인증이 아니라 아무 화면도 안 뜨는 것으로 나타난다.

"시각이 조금씩 어긋나요" 는 실제로 시계 문제다. 발급 서버와 검증 서버의 시계가 몇 초만 달라도 nbf/exp 판정이 갈린다. NTP 를 맞추고, 검증 쪽에 몇 초의 여유(clock skew)를 둔다.

조사할 때는 토큰을 그대로 로그에 남기지 않는다. 토큰은 그 자체가 자격 증명이다. 필요한 것은 sub, sid, exp 정도이므로 그것만 뽑아 남긴다.

다음 확인에서 볼 것

이 모듈은 퀴즈로 수명 설계의 판단 기준을 점검하고 코스를 마칩니다.