이 글은 아래 원문을 번역한 글로 의역이 있을 수 있습니다. 정확한 의미를 파악하고 싶으신 분은 원문을 참고해주시기 바랍니다.
원문: https://indepth.dev/posts/1382/localstorage-vs-cookies
저작권 정보: 원문 링크 표기 https://indepth.dev/
JWT토큰을 프런트엔드에 안전하게 저장하는 방법은 무엇인가? localStorage와 Cookie의 장단점을 살펴보자.
지난번 글에서는 OAuth 2.0의 구조를 살펴보고 액세스 토큰을 생성하고 토큰을 새로 고치는 방법을 조사했습니다. 이제 우리는 프런트엔드에 토큰을 저장하는 방법을 연구하고 있습니다.
Access Token은 일반적으로 서버에 의해 서명되며 요청을 승인하기 위해 서버에 대한 모든 HTTP요청에 포함된 짧은 수명의 JWT토큰입니다. Refresh Token은 일반적으로 데이터 베이스에 저장되며 만료될 때 새 access token을 얻기 위해 사용되는 긴 수명의 문자열입니다.
프런트 엔드의 토큰은 어디에 보관해야 하나요?
토큰을 저장하는 일반적인 방법은 두 가지가 있습니다.첫 번째는 localStorage 그리고 두 번째는 쿠키에 있습니다. 대부분의 사람들이 쿠키가 더 안전하다고 생각이 기울지만 어느 것이 더 나은지에 대한 많은 논쟁이 있다.
localStorage와 쿠키를 다시 한번 비교해보겠습니다.. 이 기사는 이 글과 이 글의 코멘트를 바탕으로 하고 있습니다.
로컬 스토리지
장점: 편리합니다.
- 이것은 순수한 자바스크립트이며 편리합니다.백엔드가 없고 서드파티 API에 의존하고 있다면 서드파티 API에 사이트용 특정 쿠키를 설정하도록 요청할 수 없습니다.
- 다음과 같이 헤더에 액세스 토큰을 넣어야 하는 API와 연동됩니다. Authorization Bearer ${access_token}.
단점: XSS 공격에 취약합니다.
XSS 공격은 공격자가 웹 사이트에서 JavaScript를 실행할 수 있을 때 발생합니다. 즉, 공격자는 사용자가 localStorage에 저장한 액세스 토큰을 취득할 수 있습니다. XSS 공격은 웹 사이트에 포함된 React, Vue, jQuery, Google Analytics 등의 서드파티 JavaScript 코드에서 발생할 수 있습니다. 사이트에 서드파티 라이브러리를 포함하지 않는 것은 거의 불가능합니다.
httpOnly 쿠키
장점: JavaScript를 통해 쿠키에 액세스할 수 없으므로 XSS 공격에 localstorage만큼 취약하지 않습니다.
- httpOnly와secure cookies를 사용하는 경우 JavaScript를 사용하여 쿠키에 액세스 할 수 없으므로 공격자가 사이트에서 JS를 실행할 수 있더라도 쿠키에서 액세스 토큰을 읽을 수 없음을 의미합니다.
- 모든 HTTP 요청에서 자동으로 서버로 전송됩니다.
단점: 사용 사례에 따라서는 토큰을 쿠키에 저장하지 못할 수 있습니다.
- 쿠키의 크기 제한은 4KB입니다.따라서 큰 JWT 토큰을 사용하는 경우 쿠키에 저장할 수 없습니다.
- API 서버와 쿠키를 공유할 수 없거나 API에서 권한 부여 헤더에 액세스 토큰을 넣어야 하는 경우가 있습니다. 이 경우 쿠키를 사용하여 토큰을 저장할 수 없습니다.
XSS 공격
로컬 스토리지는 JavaScript를 사용하여 쉽게 액세스할 수 있고 공격자가 액세스 토큰을 검색하여 나중에 사용할 수 있기 때문에 취약합니다. 하지만, httpOnly쿠키는 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
공격자는 숨겨진 전자 메일 필드가 있는 POST 요청을 https://site.com/email/change으로 보내는 악의적인 웹 사이트에서 쉽게 양식을 만들 수 있습니다. 그러면 세션 쿠키가 자동으로 포함됩니다.
단, 이 문제는 쿠키안에 있는 sameSite 플래그와 anti-CSRF토큰을 포함함으로써 쉽게 완화할 수 있습니다.
cookie는 local Storage에 비해 권장됩니다.
쿠키에는 아직 몇 가지 취약성이 있지만, 가능하다면 localStorage에 비해 비교적 선호되는데 왜일까요?
- localStorage및 쿠키는 둘 다 XSS 공격에 취약하지만 httpOnly쿠키를 사용하는 경우 공격자가 공격을 가하기가 더 어렵습니다.
- 쿠키는 CSRF 공격에 취약하지만 sameSite 플래그와 anti-CSRF토큰을 활용하여 완화할 수 있습니다.
- Authorization: Bearer헤더를 사용하거나 JWT가 4KB보다 큰 경우에도 사용할 수 있다.
이는 OWASP 커뮤니티의 권고와도 일치합니다.
데이터는 JavaScript에서 항상 액세스 할 수 있으므로 세션 식별자를 로컬 저장소에 저장하지 마십시오. 쿠키는 httpOnly 플래그를 사용하여 이 위험을 완화할 수 있습니다.
- OWASP: HTML5 보안 관련 참고 자료
그러면 쿠키를 사용하여 OAuth 2.0 토큰을 유지하려면 어떻게 해야 하나요?
요약하자면, 토큰을 저장할 수 있는 다양한 방법은 다음과 같습니다.
- 옵션 1: 액세스 토큰을localStorage에 저장(리프레시 토큰은 localStorage 또는 httpOnlycookies에 저장): XSS 공격으로부터 액세스 토큰이 도난당하기 쉽습니다.
- 옵션 2: 액세스 토큰 및 리프레시 토큰을 httpOnlycookie에 저장: CSRF에 걸리기 쉽지만 완화시킬 수 있고 XSS에 대한 노출에 있어서는 약간 더 나을 수 있습니다.
- 옵션 3: 리프레시 토큰을 httpOnly cookie에 저장: CSRF로부터 안전하며 XSS에 대한 노출이 다소 우수합니다.
Option 3은 3가지 옵션 중 가장 좋은 것이므로 어떻게 동작하는지에 대해 설명합니다.
액세스 토큰을 메모리에 저장하고 새로 고침 토큰을 쿠키에 저장합니다.
액세스 토큰을 메모리에 저장한다는 것은localStorage또는cookies에 넣는 대신 단순히 변수에 저장한다는 것을 의미합니다(예:const accessToken = XYZ)
이것이 CSRF로부터 안전한 이유는 무엇입니까?
/refresh_token에 form을 제출하면 작동하며 새 액세스 토큰이 반환되지만 공격자가 HTML 양식을 사용하는 경우 응답을 읽을 수 없습니다. 공격자가 성공적으로 fetch 또는 AJAX요청을 만들고 응답을 읽지 못하도록 하려면 인증되지 않은 웹 사이트의 요청을 방지하기 위해 Authorization Server의 CORS 정책을 올바르게 설정해야 합니다.
그럼 이게 어떻게 작동할까요?
1단계: 사용자 인증 시 액세스 토큰과 리프레시 토큰을 반환합니다.
사용자 인증 후 Authorization Server는access_token및 refresh_token을 반환합니다. access_token은 response body에 포함되고 refresh_token은 쿠키에 포함됩니다.
refresh token 설정 :
- httpOnly플래그를 지정하여 JavaScript가 플래그를 읽을 수 없도록 합니다.
- secure=true플래그를 지정하면 HTTPS 경유로만 송신할 수 있습니다.
- sameSite=strict 가능한 한 플래그를 설정하여 CSRF를 방지합니다. 이것은 Authorization Server가 프런트엔드와 같은 사이트를 가지고 있는 경우에만 사용할 수 있습니다. 그렇지 않은 경우, Authorization Server 는 백엔드에 CORS 헤더를 설정하거나 다른 방법을 사용하여 권한 있는 웹 사이트에서만 토큰 새로 고침 요청을 수행할 수 있도록 해야 합니다.
2단계: 액세스 토큰을 메모리에 저장합니다.
토큰을 메모리에 저장하는 것은 프런트엔드 사이트의 변수에 이 액세스 토큰을 넣는 것을 의미합니다(예:const accessToken = xyz). 이는 사용자가 탭을 바꾸거나 사이트를 새로 고치면 액세스 토큰이 사라짐을 의미합니다. 그래서 리프레쉬 토큰이 있는 것이다. 공격자가 데이터를 덤프 하여 XSS 공격을 통해 데이터를 도난당하기 쉽기 때문에 이 액세스 토큰을 JavaScript를 통해 localStorage 또는 쿠키에 넣지 않습니다.
3단계: 리프레시 토큰을 사용한 액세스 토큰 갱신
액세스 토큰이 없어졌거나 유효기간이 지났을 때/refresh_token엔드포인트 및 스텝1에서 쿠키에 저장되어 있던 리프레시 토큰이 request에 포함됩니다. 이제 새로운 액세스 토큰을 얻을 수 있으며 API 요청에 사용할 수 있습니다. 즉, JWT 토큰은 4KB보다 클 수 있으며 Authorization 헤더에 넣을 수도 있습니다.
That’s It!