이번 포스트에서는 우아한테크코스 팀 프로젝트에서 리프레시 토큰을 적용하게 된 계기를 정리해본다.
배경
우아한기크코스 팀 프로젝트 회의 중 현재 우리가 사용하고 있는 인증 인가 방식을 현업에서도 유효한가에 대해 의논한 적이 있다.
당시 팀 프로젝트의 인증 인가는 JWT 액세스 토큰을 세션 스토리지에 저장해놓는 방식으로 운영하고 있었다.
문제 상황
이 방식에는 크게 두가지 문제가 있다고 생각했다.
먼저 세션 스토리지에 저장할 경우 브라우저를 닫으면 저장된 토큰 정보가 날아간다.
대부분의 서비스는 탭이나 브라우저를 닫는다고 로그아웃되지 않는다.
우리는 세션 스토리지에 사용자 인증 정보를 담아서는 안되겠다고 판단했다.
대안 비교하기
브라우저를 닫아도 사용자가 로그인 한 상태가 유지되려면 사용자 인증 정보를 세션 스토리지가 아닌 다른 곳에 저장해야 했다.
쿠키나 로컬 스토리지가 가능한 선택지였다
쿠키
먼저 쿠키에 대해 고민해보자.
쿠키의 가장 큰 문제점은 CSRF 공격이 위험하다는 점이다.
쿠키는 매번 HTTP 요청에 같이 포함되서 가기 때문에 공격자가 [POST] /review/1
이런 URL을 실행시키도록 유도해서 사용자가 의도하지 않은 행동을 실행시킬 수 있다.
서버에서는 같이 온 쿠키로 사용자 정보를 확인에 성공했으니 더 의심하지 않고 요청을 수행하게 되서 문제가 된다.
반면 쿠키는 httpOnly
속성을 활용하면 xss 공격을 막을 수 있다.httpOnly
속성을 사용하면 자바스크립트로 접근이 불가능하기 때문에 xss 공격으로 스크립트를 실행해도 쿠키의 내용을 알지 못한다.
로컬 스토리지
반면 로컬 스토리지는 어떨까?
로컬 스토리지는 세션 스토리지와 함께 HTML5 스토리지라고 불린다.
로컬 스토리지를 사용하게 되면 세션 스토리지와는 다르게 브라우저를 닫아도 정보가 사라지지 않는다.
프론트엔드는 로컬 스토리지에 담겨있는 토큰을 꺼내서 Authorization
헤더에 담아 요청을 보낸다.
이 방법은 CSRF 공격에 비교적 안전하다.
왜냐면 쿠키와 다르게 자동으로 요청에 함께 포함되지 않고 자바스크립트로 로컬 스토리지로 꺼내서 Authorization
헤더에 담는 추가적인 행위가 필요하므로 CSRF 공격으로는 하기 어렵다.
하지만 로컬 스토리지(뿐만 아니라 HTML5 스토리지 모두) XSS 공격에 취약하다는 문제가 있다.
쿠키와 다르게 로컬 스토리지는 애초에 자바스크립트에서 사용하기 위해 등장한 개념이라 httpOnly
를 적용한 쿠키처럼 자바스크립트의 접근을 막을 수 없다.
그래서 다음과 같은 HTML 코드를 삽입하는 XSS 공격에 취약할 수 있다.
1 | <script>alert(localStorage.getItem('access-token'))</script> |
지금까지 대안을 정리하면 다음과 같다.
비교 | 세션 스토리지 | 로컬 스토리지 | 쿠키 |
---|---|---|---|
회원 정보 유지 | 불가능 | 가능 | 가능 |
CSRF 공격 | 방어 | 방어 | 취약 |
XSS 공격 | 취약 | 취약 | httpOnly 사용시 방어 |
대안 선택 기준
우리는 대안을 선택할 때 다음과 같은 기준을 정해두고 생각했다.
- 브라우저를 닫아도 회원 정보가 유지될 것
- CSRF 공격과 XSS 공격을 최대한 막아볼 것
- 만에 하나 액세스 토큰이 탈취되었더라도 피해를 최소화 할 것
- 인증 인가를 통한 DB 부하를 줄일 수 있을 것
먼저 1번을 고려했을 때 세션 스토리지(기존 방식)은 제외해야 했다.
2번을 고려하면 로컬 스토리지와 쿠키 모두 각자 다른 곳에서 취약함을 알 수 있었다.
다만 로컬 스토리지 XSS 공격은 리액트에서 어느정도 막아줄 수 있다. 하지만 리액트에 의존해서 XSS 공격을 막는 것은 부족하다고 생각했다.
반면 쿠키의 CSRF 공격은 추가적인 방어 수단을 고려해야 한다.
쿠키의 CSRF 공격 취약성은 CSRF 토큰을 도입해서 해결할 수 있다고 한다.
CSRF 토큰은 난수를 서버에서 저장하고 매 요청마다 클라이언트가 난수를 같이 보내서 서버에서 난수가 일치하는 지 확인하는 방식이다.
문제는 난수를 서버에서 관리하는 비용이 있고 만약 난수를 DB에서 관리할 경우 매 요청마다 DB에 접근해서 난수를 비교해야하는 비용도 존재했다.
또한 CSRF 공격을 referer 헤더를 체크하는 방식으로 막을 수도 있다.
referer는 공격자가 위조할 수 있는 요소라서 referer 체크로는 CSRF 공격을 막기 힘들다고 생각했다.
3번은 우리가 예상하지 못한 경우를 대비한다.
즉 어찌됐건 토큰이 유출됐을 경우 피해를 최소화하기 위해서는 액세스 토큰의 유효 시간을 짧게 설정할 필요가 있었다.
하지만 액세스 토큰 유효시간을 짧게 설정하면 너무 빠르게 로그아웃되어버리는 현상이 발생할 수 있었다.
이럴 때 사용되는 것이 리프레시 토큰이다. 리프레시 토큰은 다른 포스트로 이어서 설명해보겠다.
4번은 간단하다 보안을 생각하는 것은 좋으나 그렇다고 지나치게 DB 서버에 부하를 주는 방식은 곤란하다. 왜냐면 우리 프로젝트의 서비스가 금융과 관련된 보안에 민감한 서비스는 아니기 때문에 보안 때문에 성능을 포기하면 안된다고 생각했다.
4번 관점에서는 CSRF 토큰을 사용하기는 어려웠다.
대안 선택 하기
우리가 선택한 방법은 리프레시 토큰을 httpOnly 쿠키에 담고 액세스 토큰을 자바스크립트 변수로 관리하는 방식이었다.
이렇게 하면 우리의 선택 기준을 충족할 수 있었다.
- 브라우저를 닫아도 쿠키에 저장된 리프레시 토큰으로 다시 액세스 토큰을 발급받으면 되니 회원 정보를 유지할 수 있었다.
- XSS 공격은
httpOnly
속성으로 방지하고 CSRF 공격은 리프레시 토큰만으로는 액세스 토큰 발급을 제외한 다른 행위를 할 수 없으니 해커의 공격 가능한 범위를 줄이는 방식으로 대응했다.
만약 액세스 토큰을 발급하는 데 성공해도 해커는 이 응답값에 접근할 수 없으므로 큰 위협이 되지 않는다. - 액세스 토큰이 만에 하나 갈취되어도 짧은 유효시간을 적용하면 해커가 공격 가능한 시간이 적어져 대응 가능하다고 판단했다.
만약 유효시간이 훨씬 긴 리프레시 토큰이 갈취 되었을 경우에는 액세스 토큰을 발급하면 만료시키는 방식으로 대응했다.
이렇게 하면 리프레시 토큰이 갈취되어도 액세스 토큰 한 개의 시간 만큼만 공격에 노출된다. - 리프레시 토큰을 통해 액세스 토큰을 발급받을 때 추가적인 DB 접근 비용이 필요했다.
하지만 CSRF 토큰을 도입했을 경우처럼 매 요청마다 DB를 접근하는 것이 아닌 액세스 토큰을 발급할 때만 DB에 접근해서 감당 가능하다고 봤다.