우선 토큰 저장 방식을 정하기 전에 배경지식에 해당하는 웹 공격에 해당하는 XSS와 CSRF를 알아보자.
웹 공격
XSS
공격자가 브라우저에 자바스크립트를 삽입해 실행하는 공격으로 크게 두가지 방식이다.
<input>
을 통해 자바스크립트를 서버로 전송해 서버에서 스크립트 실행
- url에 자바스크립트를 적어 실행
두 방식을 통해 사이트의 글로벌 변수값등을 가져와 사이트인 척 API콜을 요청하는 것이다.
즉, 공격자는 사이트의 로직인 척 원하는 코드를 실행하는 것이다.
CSRF
공격자가 다른 사이트에서 우리 사이트의 API 콜을 요청해 실행하는 공격으로 클라이언트 도메인이 누구인지 서버에서 통제하고 있지 않을때에 가능하다.
이때 공격자가 클라이언트에 저장된 유저 인증 정보를 이용해 서버에 보내 로그인한 흉내를 내 API요청을 보내는 것이다.
JWT토큰을 어디에 저장할지에 대한 고민
zustand(not persist)
새로고침 시 전역상태가 날라가므로 안됨
zustand(persist)
새로고침해도 state 유지되지만 결국 로컬스토리지나 세션 스토리지에 저장하는 것과 동일
세션 스토리지
페이지를 새로고침하거나, 이동하여도 토큰이 유지되지만 새로운 탭에서 접속 시 세션이 나뉘어지고, 브라우저가 종료되는 순간 휘발되어 사용자 경험에 좋지 않다.
모든 자바스크립트 코드를 통해 액세스 할 수 있음으로 XSS 공격에도 취약하지만 자바스크립트 코드로 제어가 필요하기에 CSRF 공격에서는 안전하다.
로컬스토리지
페이지를 이동하거나 브라우저를 닫아도 토큰이 유지되지만 역시나 자바스크립트 코드를 통해 액세스 하므로 XSS 공격에 약하고 CSRF 공격에 안전하다.
비공개 변수
가장 안전하지만 페이지를 이동하거나, 새로고침만 해도 정보가 날아가므로 단독적으로 사용이 불가능하다.
쿠키
옵션이 없으면 XSS, CSRF공격에 모두 취약함.
하지만, 백엔드와 합을 맞추면
httpOnly
secure
SameSite
옵션등을 사용하면 보안을 강화할 수 있다.- httpOnly
- 스크립트 상에서 접근이 불가능하도록 막음 → XSS 공격 방어
- 하지만 네트워크 감청을 통해 평문으로된 쿠키를 볼 수 있음
- 매 요청마다 쿠키가 전송되므로 아직 CSRF 공격에 취약
- secure
- 패킷 감청을 막기 위해 https 통신 시에만 쿠키 사용
- 프론트와 로그인 API를 제공할 백엔드는 같은 도메인을 공유해야한다.
- SameSite
- Strict
- 같은 도메인에서만 해당 쿠키 사용하게 함
- Lax
- 페이지 이동 시 혹은 Form을 통한 Get요청 시에만 허용
즉, 백엔드측에서 다음과 같은 설정이 필요하다.
여기에 더불어 백엔드에서 referer체크와 double submit 체크, CSRF 방어를 하면 CSRF공격도 막을 수 있다고 한다.(자세한 건 잘 모른다)
최종적으로
accessToken
(JSON Payload)은 웹 어플리케이션에서 비공개 변수로 관리하고, refreshToken
은 쿠키로 전달받아 쿠키로 관리하는 것이 최선의 방법이다.대부분의 웹 서비스는 서버에서 요청을 보낼 수 있는 클라이언트의 주소를 한정하기 위해 요청에 담긴 헤더를 검증하므로 모르는 클라이언트에서 요청을 가장한 공격을 막을 수 있어 CSRF공격을 통해 우회하려면 클라이언트 주소를 위조하는 방법밖에 없는데 실제로 공격자가 보낸 페이지로 응답이 오지 않기 때문에
refreshToken
은 httpOnly 쿠키로 저장해도 액세스 토큰을 탈취할 기회가 없다.결론
보안과 사용자 경험 모두를 챙길 수 있는 과정은 다음과 같다.
로그인 시 액세스 토큰(reponse body)과 리프레쉬 토큰(set-Cookie의 헤더)을 응답으로 받음 →액세스 토큰은 비공개 변수에 저장, 리프레쉬 토큰은 httpOnly쿠키로 저장
- 인가가 필요한 요청은 헤더에 액세스 토큰 담아서 보냄, 만약 액세스 토큰이 만료되면 쿠키에 저장된 리프레쉬 토큰을 검증해서 새로운 액세스 토큰을 서버로 부터 받고 원래 요청을 다시 보냄
- 새로고침하거나 재접속 시에도 서버에 리프레쉬 토큰을 보내 새로운 액세스 토큰을 발급 받는다.(리프레쉬 토큰이 존재하고 유효하면 액세스 토큰을 저장해 자동으로 로그인 상태로 만듦) 존재하지 않거나 만료되면 재로그인 시킨다.
보안도 중요하지만 멘토님이 현업에서 비공개 변수에 토큰을 저장하는건 고려대상이 아니라고 하셨다. 일반적이지 않은 방법인듯
또한 토큰을 일관적이지 않게 쿠키/웹 스토리지로 나눠서 관리하면 유지보수도 힘들고 공을 꽤 들여야한다고 하니 가장 일반적인 방법인 로컬스토리지에 access Token과 refresh Token을 둘다 담고 accessToken이 만료시 자동으로 refreshToken을 통해 accessToken을 Axios Interceptor를 통해 재발급받아 재 요청을 보내는 방식을 해보자.
다른 프로젝트 참고
데브코스 4기

쿠키가 아닌 스토리지에 accessToken과 refreshToken을 관리하는 모습, 받아오는 과정도 위에서 언급한 방식과 거의 일치
우테코 5기 크루
5개?정도 프로젝트를 써본결과 accessToken은 로컬에서 관리하고 refreshToken은 쿠키가 좀 많이 보였고 로컬에서 관리하는 것도 종종 보였다.
쿠키같은 경우 옵션은 SameSite:None, HttpOnly는 선택으로 사용하는듯 했다.
Refresh Token이 만료되면 재발급 vs 재로그인

만료 기한이 1~2주 정도로 길다면 재로그인이 좋아보임.
참고자료
https://ppaksang.tistory.com/16 —> 백엔드 입장