7 - Access Token과 Refresh Token
Updated:
Categories: ci-cd-zero-downtime-auth
워밍업
JWT Token 프로젝트를 진행하면서 많은 어려움과 고민을 안겨다준 AccessToken과 RefreshToken. 서로 상호보완하는 두 토큰을 활용해서 인증/인가 플로우대로 기능 구현한다는 게 참 쉽지 않았던 일이었던 것 같다.
왜냐하면, 도입 및 구현까지 알아야할 것들도 많았고, 특히 인증/보안 부분은 정해진 답이 없었기 때문이다. 예를 들어 pc 기기는 고정된 IP로 사용될 일이 많다 보니 IP 보안 기능이 필요하거나 아니면 토큰을 중간 탈취 시 이를 어떻게 대응해야할지라던가? 또 이메일 인증 보안 기능을 도입한다면 어떻게 토큰을 운용해야할지 정말 끝 없는 고민 속에서 허우적대기만 했던 것 같다.
그렇지만, 이 과정 속에서 나는 JWT 토큰 개념과 인증/인가에 대해서 한발짝 더 나아갈 수 있게 됐다. 그래서 이번에 정리, 고민, 해결 방식을 기록할 목적으로 작성하려고 한다.
단일 Access Token 운용 문제점
Access Token은 서버 인증 이후에 사용자에게 제공되는 인증과 권한을 나타내는 단기 유효 토큰을 말한다.
사용자는 상시 서버에 API 요청을 보낼 때 이 토큰을 Authorization Header의 방식 중 하나인 Bearer에 붙여 증명한다. 이를 통해 중간에 payload를 변조하더라도 검증 단계에서 이를 알 수 있기 때문에 보안이 가능하다.
하지만, 서버는 사용자를 식별할 수 없다. 그렇기 때문에 제 3자가 탈취된 토큰으로 인증하더라도 이를 인지할 수 없을 뿐더러 설령 이미 알고 있더라도 주도권은 소유자에게 있으므로 아무런 조치를 취할 수가 없다.
이를 해결하기 위해 유효 기간을 몇 분으로 매우 짧게 설정했지만, 잦은 로그인 요청은 서버와 사용자 모두에게 불편을 초래할 수 있다. 따라서 이를 보완하고자 나온 토큰이 바로 Refresh Token이다.
Refresh Token 필요성
이 토큰은 새로운 Access Token을 재발급 목적으로 사용되는 토큰이다.
Access Token이 짧은 만료 기간을 가지고 있다면 그와는 반대로 Refresh Token은 일반적으로 최소 2주 정도의 긴 만료 기간을 가지고 있다.
Refresh Token은 Access Token과 반대로 주도권이 서버에게 있다. 따라서 탈취 당하더라도 이를 Blacklist 보안 기능을 통해 탈취한 Refresh Token을 사용해서 Access Token을 재발급하는 행위를 방지할 수 있다. 그런데, 여기서 의문점이 한 가지가 든다면 바로 stateful 특성이다. JWT 토큰은 분명 stateless일텐데 서버가 관리하게 되면서 Session 방식과 같아지기 때문이다. 이는 여러 가지로 논란이 많지만 내 생각엔 JWT 토큰 자체는 stateless가 맞지만, 보안 처리 과정에서 어쩔 수 없이 저장할 수 밖에 없었던 문제이기 때문에 인증/인가 과정은 stateful이 맞다. 그렇지만, session과 완전히 같지는 않다. 왜냐하면 서버 저장 환경을 Redis에 저장함으로써 단점들을 상쇄했기 때문이다.
왜 Redis인가?
TTL(Time-To-Live)
토큰에 만료일을 지정할 수 있고 자동 삭제가 되기 때문에, 리소스를 효율적으로 관리할 수 있다. 이는, 기존 RDB 저장 방식에서 수동적으로 관리하는 것보다 획기적이다.
In Memory DB
Access Token 만료 시 이를 재발급할 경우에 Redis는 데이터베이스가 아닌 메모리에서 RefreshToken을 조회 및 쓰기 작업을 하기 때문에 병목 현상을 방지할 수 있다. 왜냐하면 데이터베이스에서는 요청이 많으면 디스크 I/O나 쿼리문 그리고 토큰 복호화 처리 시간이 상당한 부하를 줄 수 있기 때문이다. 또한, Redis는 자주 조회되는 데이터나 연산 처리를 캐싱 기능을 사용해서 더욱 성능을 개선시킬 수 있다.
- RDB와 Redis 속도 비교
- 조만간 성능 측정을 직접 해보려고 하지만, 당장 근거가 필요하기 때문에 여러 블로그의 RDB와 Redis 부하 테스트 내용을 참고해서 얼마나 차이가 나는지 확인해 보았는데, 대략 평균적으로 최소 20배 ~ 100배 이상의 성능 차이를 보였다고 한다.
Trade Off
In Memory는 휘발성 메모리이다. 따라서 데이터를 보존할 수가 없다는 것이 큰 치명적 단점이다. 하지만, JWT Token 시스템에서 Redis에 저장하는 데이터는 Refresh Token이고, 서버가 취해야할 가장 큰 액션은 겨우 재로그인 요청일 뿐이다. 따라서 손실보다 얻는 것이 많다고 할 수 있다.
Access, Refresh 저장 관리
클라이언트에서 저장 가능한 방식은 4가지가 있다.
- 로컬 스토리지(Local Storage)
- 사용자 브라우저에 세션을 유지하지 않더라도 데이터가 영구적으로 보관되는 방법이다. 하지만, 이 방법은 XSS(교차 사이트 스크립팅) 공격에 취약하기 때문에 토큰이 탈취될 수 있다.
- 세션 스토리지(Session Storage)
- 이름 그대로 세션 유지 상태에서만 데이터를 보존할 수 있는 저장소다. 로컬 스토리지보다는 보안성이 더 좋다고 할 수는 있지만.. XSS 공격에 취약한 건 매한가지다.
- 쿠키(Cookie)
- 쿠키는 HTTP의 무상태(Stateless) 특성을 보완하기 위해 나타났고, key:value 형태의 자료구조다. 쿠키는 두 가지로 구분되는데, 만료 날짜를 지정할 수 있는 영구적 쿠키와, 세션 특성의 세션 쿠키로 구분된다. 상황에 따라서 적절한 쿠키를 사용하면 될 것이다.
위에 두 스토리지는 XSS 공격에 취약하지만 쿠키는 예방할 수 있다. 바로 HttpOnly 플래그를 사용하여 J.S 공격을 방지할 수 있기 때문이다. 또한, Secure 플래그를 사용하면 HTTPS 연결에서만 쿠키를 전송되게 하여 중간자 공격(MITM)을 막을 수 있고, SameSite 쿠키 속성을 설정하면 CSRF(교차 사이트 요청 위조) 공격을 방어할 수 있다. 하지만 이 SameSite는 CSRF 토큰을 공유해야 하므로 JWT 토큰에서는 사실상 안쓴다.
- 메모리(Memory)
- 클라이언트 서버의 메모리에 저장하는 방식이다. 그러니까 코드 변수에 토큰을 저장하는 것이다. 이렇게 하면 API 호출 시 Access Token에 접근하기 쉬워지지만, 브라우저의 메모리는 세션 단위로 관리되기 때문에 페이지를 이동하면 데이터는 사라지게 된다. 따라서 이 방식은 SPA(Single Page Application)에서 주로 사용된다고 한다. 서버 메모리에 데이터를 저장하면 스크립트가 메모리 공간을 직접적으로 제어할 수 없기 때문에 XSS 공격에 의한 탈취 위험이 상대적으로 적다.
그래서 각 토큰을 어떤 저장 방식에 저장했을까?
Access Token 저장 위치
저장을 하지 않고 응답 값으로 클라이언트 측으로 보내기만 했다. 그 이유는 프론트 개발을 하지 않았기 때문이다. 그러나 만약에 저장하려고 한다면 세션 스토리지에 저장하려고 할 것이다. 왜냐하면 Access Token은 만료 시간이 5분도 채 되지 않기 때문에 로컬 스토리지에 저장할 필요가 없고 그리고 XSS 공격에 취약하지만 사실상 금방 만료되기 때문에 해커가 할 수 있는 일이 많지 않다. 또한, CSRF는 단 한 번의 공격으로 매우 치명적일 수 있다는 점을 고려했기 때문에 쿠키에 저장하지 않았다.
Refresh Token 저장 위치
나는 쿠키에 담아 전송했다. 그 이유는 CSRF 공격으로 피해를 받을 수 없고, 그리고 반대로 XSS 공격에는 탈취될 수 있어 위험하지만, 이를 HttpOnly, Secure 두 옵션을 사용해서 방지할 수 있다.
댓글남기기