LocalStorage vs. Cookies: JWT 토큰을 안전하게 저장하기 위해 알아야할 모든것
안녕하세요 백엔드 개발자 최준혁입니다.
JWT 토큰을 front-end에 어떻게 안전하게 저장할 수 있는지 LocalStorage와 Cookies의 장단점에 대해서 한 번 살펴보자.
Access token은 보통 짧은 수명 주기를 가지고, 당신의 서버로부터 승인을 받아 인증이 필요한 모든 HTTP request에 포함된다.
그에 반해 Refresh token은 보통 긴 수명 주기를 가지고, 당신의 DB 저장돼서 access token이 만료되었을 때 새로운 access token을 발급해주는 토큰이다.
Front-end에서 토큰을 어디에 저장해야 할까?
토큰을 저장하기 위한 방법은 일반적으로 두가지 방식이 존재한다. 첫번째는 localStorage
에 저장하는 방식이고, 두번째는 cookies
에 저장하는 방식이다. 이 두가지 방식에 대해서 어떤 방식이 더 나은지에 대해 많은 사람들이 논쟁을 하지만, 대부분의 사람들은 쿠키에 저장하는 방식이 더 안전하다고 얘기한다.
그럼 localStorage
와 cookies
를 비교해보자.
LocalStorage 장단점
장점: 편리하다.
LocalStorage는 pure JavaScript로서 편리한 장점이 있다. 만약 당신이 back-end를 가지고 있지 않고 third-party API에 의존하고 있다면, 매번 third-party API에 쿠키를 세팅할 수 없을것이다.
LocalStorage에 저장된 access token은 Authorization Bearer ${access_token}
과 같은 형식으로 HTTP header에 넣어서 사용해야 한다.
단점: XSS 공격에 취약하다.
XSS 공격은 당신의 웹사이트에서 공격자가 JavaScript를 실행할 수 있을때 발생한다. 이는 localStorage
에 저장되어있는 당신의 access token을 공격자가 탈취할 수 있다는 말이다. XSS 공격은 React, Vue, jQuery, Google Analytics 등과 같은 당신의 웹사이트에 포함된 third-party JavaScript 코드에 의해서 발생할 수 있다. 하지만 third-party 라이브러리를 당신의 웹사이트에 포함하지 않는 것은 거의 불가능하다.
Cookies 장단점
장점: 쿠키는 JavaScript로 접근이 불가능하다. 그래서
localStorage
만큼 XSS 공격에 취약하지 않다.
만약 공격자가 JavaScript를 당신의 사이트에서 실행한다고 했을 때, cookie의 httpOnly
나 secure
옵션을 사용한다면 당신의 쿠키는 JavaScript의 접근에 안전해진다.
그리고 쿠키는 자동으로 모든 HTTP 요청에 포함되어 보내진다.
단점: 일부 케이스에 대해서는 당신의 토큰을 쿠키에 저장하지 못할 수도 있다.
쿠키는 4KB의 size limit을 가진다. 그러므로 만약 당신이 4KB가 넘는 데이터를 가지는 JWT 토큰을 사용한다면 쿠키는 적절한 선택지가 아닐 수도 있다.
만약 당신의 API 서버가 쿠키를 사용할 수 없거나 API의 요청에서 access 토큰을 authorization header에 넣어줘야 한다면 당신은 쿠키를 사용할 수 없을 것이다.
XSS 공격에 대해서
LocalStorage는 JavaScript를 사용해서 쉽게 접근이 가능하기 때문에 취약하지만, httpOnly
를 적용한 cookie는 JavaScript로 접근이 불가능하다. 하지만 이것이 XSS 공격에 완전히 안전하다는 소리는 아니다.
만약 공격자가 당신의 애플리케이션에서 JavaScript를 실행할 수 있을 때, 자동적으로 당신의 쿠키에 포함되는 HTTP 요청을 당신의 서버로 보낼 수 있다. 이때 공격자는 토큰의 정보를 읽을 수 없지만, 읽을 필요 또한 없기 때문에 크게 문제 될 부분이 없다. 또한 공격자의 컴퓨터를 사용하는 것보다 공격 대상자의 브라우저를 사용하는 것(단순히 HTTP 요청을 보내는 것)이 더 유리할 수도 있다.
쿠키와 CSRF 공격
CSRF 공격은 유저가 의도하지 않은 요청을 하도록 만드는 공격이다. 예를들어, 만약 웹사이트가 이메일 변경 요청을 아래와 같이 받는다고 해보자.
POST /email/change HTTP/1.1
Host: site.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 50
Cookie: session=abcdefghijklmnopqrstu email=myemail.example.com
그럼 공격자는 쉽게 악의적인 웹사이트에서 form을 만들 수 있다. 이 form은 POST 요청으로 https://site.com/email/change
로 보내지게 되는데, hidden email 필드와 session cookie가 자동으로 포함되어져서 보내진다.
하지만 쿠키에 sameSite
플래그를 사용하고, anti-CRSF token
을 사용하면 이 문제를 쉽게 해결할 수 있다.
localStorage보다 Cookies가 더 좋다(?)
cookies
가 취약점이 여전히 존재함에도 불구하고 localStorage
보다 더 선호된다. 왜일까?
localStorage
와cookies
둘 다 XSS 공격에는 취약하다. 그러나 당신이 쿠키에서httpOnly
플래그를 사용한다면 쿠키가 조금 더 공격자가 접근하기 어렵다.- 쿠키는 CSRF 공격에 취약하다. 그러나
sameSite
플래그와anti-CSRF tokens
를 사용한다면 어느정도 예방시킬 수 있다. - Authorization: Bearer header를 사용해야 하거나 JWT의 크기가 4KB보다 큰 경우에도 작동 가능하게 할 수 있다.
OWASP community에서도 아래와 같은 조언을 하고 있다.
Do not store session identifiers in local storage as the data is always accessible by JavaScript. Cookies can mitigate this risk using the httpOnly flag.
- OWASP: HTML5 Security Cheat Sheet
그래서 OAuth 2.0 토큰을 유지하기 위해서 쿠키를 어떻게 사용해야 할까?
앞의 내용을 다시 한 번 상기시키기 위해서 당신의 토큰을 저장하는 방법 3가지에 대해서 살펴보자.
- Option 1: 당신의 access token을
localStorage
에 저장하라(refresh token은localStorage
나httpOnly
cookies에 저장하라): access token은 XSS attack으로부터 취약하다. - Option 2: 당신의 access token과 refresh token을
httpOnly
cookies에 저장하라: CSRF 공격에는 취약하지만 어느정도 예방 가능하고, XSS의 노출 면에서는 조금 더 낫다. - Option 3: 당신의 refresh token을
httpOnly
cookie에 저장하라: CSRF 공격으로부터 안전하고, XSS 노출에 대해서는 조금 더 낫다.
이 세가지 option 중에서 3번째 옵션이 어떻게 작동 하는지 한 번 살펴보자.
당신의 access token은 memory에 저장하고, refresh token은 cookie에 저장한다.
access token을 메모리에 저장하라는 말의 의미는
localStorage
나cookies
에 저장하는 것 대신에 access token을 variable에 넣으라는 말이다.(예를들어, const accessToken = XYZ)
왜 CSRF 공격으로부터 안전할까?
/refresh-token
에서는 form이 제출되고 새로운 access token을 돌려주지만, 공격자가 HTML form을 사용한다고 하면 response를 읽을 수 없다.
공격자가 성공적으로 fetch나 AJAX 요청을 하거나 response를 읽는 것을 방지하려면 Authorization server의 CORS 정책을 인증되지 않은 웹사이트로부터 받지 않게 세팅을 잘 해두어야 한다.
그래서 어떻게 셋업을 해야하나요?
Step 1: 유저가 인증할 때 access token과 refresh token을 반환한다.
유저가 인증하고 난 후, Authorization server는 access_token
과 refresh_token
을 반환한다. access_token
은 response body에 포함되고, refresh_token
은 cookies에 포함된다.
Refresh token cookie setup:
httpOnly
플래그를 사용하면 javaScript가 쿠키를 읽는 것을 방지해준다.secure=true
플래그를 사용하면 HTTPS에서만 요청을 보낼 수 있다.- 가능하면
sameSite=strict
플래그를 사용해서 CSRF를 방지해라. 이 플래그는 Authorization server가 당신의 프론트엔드와 같은 사이트 일때만 사용할 수 있다. 만약 이 경우가 아니라면 당신의 Authorization server는 CORS header를 백엔드에서 설정하거나 refresh token 요청을 인증된 웹사이트에서만 완료시킬 수 있도록 어떠한 방법으로든지 세팅해주어야 한다.
Step 2: 메모리에 access token을 저장한다.
토큰을 in-memory에 저장한다는 것의 의미는 당신의 access token을 front-end에서 variable에 넣어두는 것을 뜻한다.(예를들어 const accessToken = xyz
와 같이)
만약 유저가 탭을 전환하거나 페이지를 새로고침 한다면 당신이 생각하는 것처럼 access token은 사라지게 될 것이다. 그래서 바로 refresh token이 필요한 것이다. 공격자가 JavaScript로 데이터를 탈취하기 쉽기 때문에 access token을 localStorage
나 cookie
에 넣어두지 않는 것이다.
Step 3: refresh token을 이용해서 access token을 갱신한다.
access token이 사라지거나 만료되었을 때, step 1에서와 같이 cookie에 저장한 refresh token을 /refresh-token
endpoint로 보내준다. 그럼 당신은 새로운 access token을 받게 될 것이다. 그리고 그것을 새로운 API 요청에 사용할 수 있다. 이는 당신의 JWT token은 4KB보다 더 클 수 있고, 이것을 Authorization Header에 포함시킬 수도 있다는 것을 의미한다.