모바일 앱 API 인증을 JWT 방식으로 바꾸면서 서버가 상태를 안 갖게 됐고 세션 저장소도 필요 없어졌다. 그 상태에서 로그아웃을 구현하다 막혔다.
Table of contents
Open Table of contents
증상 — 지울 것이 없는 로그아웃
세션 방식이면 서버의 세션을 지우는 것으로 로그아웃이 끝난다. JWT 방식은 토큰이 클라이언트에 있고 서버가 아무것도 갖고 있지 않다.
클라이언트에 지우라고 할 수는 있지만 그것은 요청일 뿐이다. RFC 7519 대로 exp 전까지는 토큰이 유효하므로 복사해 뒀다면 로그아웃 뒤에도 쓸 수 있다.
판단 기준 — 세 가지 대응과 선택
세 가지 방법이 있었다. exp 를 짧게 하면 위험 구간이 줄지만 갱신을 자주 해야 한다. jti 차단 목록을 두면 확실하지만 매 검증에서 저장소를 봐야 하므로 무상태라는 장점이 사라진다. 세 번째는 짧은 접근 토큰과 긴 갱신 토큰으로 나누고 로그아웃에서 refresh_tokens 의 갱신 토큰만 무효화하는 방식이었다.
세 번째 쪽을 골랐다. 이유는 접근 토큰 검증은 매 요청이고 갱신은 가끔이라 자주 하는 것을 싸게 두고 드문 것을 비싸게 두는 셈이 되어서다. 대신 접근 토큰 만료 전 몇 분은 로그아웃 뒤에도 유효한 것을 감수했다.
조치 — 갱신 시 회전이 만드는 신호
갱신 토큰을 쓸 때 새 것으로 바꾸고 옛 것을 무효화하게 했다. 한 번 쓰면 다시 못 쓰는 구조가 된다.
이렇게 하면 유출됐을 때 신호가 생기는데 공격자가 먼저 쓰면 정상 사용자의 갱신이 실패하고 반대면 공격자 것이 무효가 된다. 둘 중 하나가 반드시 실패하므로 그 실패 자체가 탐지 근거가 된다.
주의 — 시계 차이와 허용 오차
토큰 만료는 시각으로 판단하므로 발급하는 쪽과 검증하는 쪽의 시계가 다르면 문제가 된다. 검증 쪽이 조금 빠르면 방금 발급한 토큰이 nbf 검사에서 아직 유효하지 않다고 거부될 수 있다.
같은 RFC 가 시계 오차를 위해 몇 분 이내의 작은 여유를 둘 수 있다고 적고 있어서 그 여유를 뒀고 NTP 동기화 상태도 함께 확인했다. 오차가 크면 여유로 덮을 수 없으므로 여유는 동기화의 대체물이 아니다.
토큰에 담을 것을 고른다
토큰에 정보를 넣으면 매번 조회하지 않아도 되지만 두 가지 이유로 담을 것을 줄였다. 토큰은 매 요청 Authorization 헤더에 실리므로 커지면 전송량이 늘어난다.
더 중요한 것은 roles 를 담으면 권한이 바뀌어도 만료 전까지 옛 권한으로 동작한다는 점이다. 바뀌면 만료까지 기다려도 되는 것만 담고 즉시 반영돼야 하는 withdraw_locked 같은 것은 매번 조회하게 했다.
디버깅하려고 토큰을 로그에 찍을 뻔한 것도 여기서 걸렀다. 토큰은 자격증명이므로 로그에는 jti 만 남겨서 어느 토큰인지는 알되 그것으로 인증은 못 하게 했다.
정리
JWT방식의 장점인 무상태가 로그아웃에서 걸린다- 발급된 토큰은
exp전까지 유효하다 - 대응은 짧은 만료와
jti차단 목록과 갱신 토큰 분리 셋이다 - 갱신 토큰 분리는 자주 하는 검증을 싸게 만든다
- 갱신 시 회전시키면 유출이 실패로 드러난다
- 시계 차이에 허용 오차를 두되
NTP동기화를 대체하지 않는다 - 토큰에는 바뀌면 만료까지 기다려도 되는 것만 담는다
- 토큰을 로그에 찍지 않고
jti만 남긴다