
TL;DR
- 정답은 없다.
- 개인적으로 권장하는 방법은 AccessToken은 비공개 변수로, RefreshToken은 Cookie로 저장.
- Cookie는
secure
,httpOnly
등의 설정을 적용하여 XSS 공격을 방지.
보다 안전하게 사용하기
JWT를 구현하는 글은 여기저기 있고, 발급한 토큰을 어떻게 관리할지에 대한 고민을 적어보려고 합니다.
사용자의 편의성과 보안 사이에서 균형을 잡는건 항상 어려운 일입니다.
토큰 인증 방식은 편리하지만 토큰이 탈취된다면 이를 즉시 조치하기가 어렵다는 단점도 존재합니다.
따라서 안전하게 보관하는 것이 중요합니다.
웹 클라이언트에서 토큰을 보관할 수 있는 대표적인 장소를 비교해보겠습니다.
JavaScript 변수 | LocalStorage | Cookie | |
새로고침 후 유지되는가 | 휘발됨 | 유지됨 | 유지됨 |
XSS 공격에 취약한가 | private 접근자인 경우 상대적으로 안전함 |
최약함 | httpOnly 설정인 경우 상대적으로 안전함 |
CSRF 공격에 취약한가 | 상대적으로 안전함 | 상대적으로 안전함 | 취약함 |
몇 가지 비교를 해보았을 때 상대적으로 안전한 방식은 JavaScript 변수로 보관하는 것입니다.
하지만 JavaScript 변수는 사용자가 다시 접속하는 경우 모두 휘발됩니다.
사용자가 실수로 새로고침 버튼을 누르더라도 다시 로그인을 수행해야 하는 아주 치명적인 단점이 있습니다.
두 저장소를 상호 보완하여
AccessToken과 RefreshToken을 서로 다른 저장소에 보관하면 상대적으로 더 안전하게 토큰을 관리할 수 있습니다.
sakd.uk은 AccessToken을 JavaScript 변수로, RefreshToken은 쿠키에 보관하는 방식을 채택했습니다.
아래는 토큰과 관련한 몇 가지 시나리오입니다.
로그인
- 사용자가 ID와 암호로 AccessToken을 요청한다.
- DB의 계정 정보와 일치한 경우 신규 RefreshToken은 쿠키에 담아, AccessToken은 HTTP 응답으로 반환한다.
- 클라이언트는 내부 변수로 은닉하여 AccessToken을 보관한다.
재접속
- 사용자가 페이지 전환, 새로고침 등으로 인하여 새로 접속한다.
- 페이지 초기화 시 AccessToken 존재 여부를 확인한다.
- AccessToken이 존재하지 않는다면 쿠키와 함께 신규 발급을 요청한다.
- 서버는 쿠키에 담겨져 전달된 RefreshToken을 검증한다.
- 검증에 성공했다면 신규 AccessToken을 발급하여 클라이언트로 전달하고, JavaScript 내부 변수로 은닉하여 보관한다.
- 검증에 실패했다면 실패 응답을 클라이언트로 전달하고, 쿠키를 파기한다.
RefreshToken 갱신
- AccessToken 요청 중 토큰 만기 임박 여부를 확인한다.
- 만기 임박한 경우 신규 AccessToken을 요청한다
- 서버는 쿠키에 담겨져 전달된 RefreshToken을 검증한다.
- 검증에 성공했다면 신규 AccessToken을 발급하여 클라이언트로 전달하고, JavaScript 내부 변수로 은닉하여 보관한다.
- 검증에 실패했다면 실패 응답을 클라이언트로 전달하고, 클라이언트에서는 로그아웃 처리한다.
더 안전한 이유
XSS(Cross-Site Scripting) 공격
AccessToken이 JavaScript 내부 변수로 은닉되어 있다면 접근하여 탈취하기가 쉽지 않습니다.
또한 서버에서 쿠키의 httpOnly
설정을 활성화한다면 JavaScript에서 쿠키로 직접 접근할 수 없습니다. (물론 우회 방법으로 열람하는 방법이 없는 것은 아닙니다.)
때문에 AccessToken을 LocalStorage에 저장하는 것보다 변수로 보관하는 것이 안전하게 보관하는 방법이 될 수 있습니다.
CSRF(Cross-Site Request Forgery) 공격
CSRF 공격을 시도하여도 공격자가 활용할 수 있는 토큰은 RefreshToken 뿐입니다.
RefreshToken으로는 AccessToken 재발급 요청 외에는 다른 요청을 할 수 없으므로 상대적으로 안전해집니다.
또한 아래 몇 가지 설정을 활성화 하면 쿠키 사용에 제약을 걸 수 있습니다.
httpOnly
: JavaScript에서는 쿠키로 접근할 수 없다.secure
: SSL 방식으로 접속했을 때에만 쿠키가 전송된다.same-site
: 지정된 규칙의 도메인에서만 쿠키가 전송된다.
결론
토큰 저장 장소는 보안과 직결되는 중요한 문제입니다.
각 장소별로 서로 다른 취약점을 갖고 있으므로, RefreshToken을 도입한 뒤 각 토큰을 적당한 장소에 보관하는 것이 좋습니다.
참고 문서
- [Frontend] JWT 토큰을 어디에 저장해야할까? - lime.log (https://velog.io/@joohr1234/Frontend-JWT-토큰을-어디에-저장해야할까)
- JWT는 어디에 저장해야할까? - localStorage vs cookie - 0307kwon.log (https://velog.io/@0307kwon/JWT는-어디에-저장해야할까-localStorage-vs-cookie)