jwt 토큰을 이용한 로그인 구현 로직 - innovationacademy-kr/slabs-munetic GitHub Wiki

작성일/작성자 : 2022-01-11 / chaepark 다음의 글에는 보안 분야에 대한 정보가 담겨있습니다. 저는 보안에 대해서 잘 알지 못합니다. 그러니 오류나 잘못된 정보가 있다면 알려주시고 정정 부탁드립니다.

💡 jwt 토큰 사용 이유

본 프로젝트 인증/인가 로직은 jwt 토큰을 사용해 구현되었습니다(프로젝트 1팀 기준). jwt 토큰을 사용한 이유는 다음과 같습니다.

  • 세션과 달리 서버에 식별자를 저장하지 않아 서버 용량을 줄일 수 있습니다.
  • 서버에 저장하지 않기 때문에 서버 확장에 용이합니다.

반면 jwt 토큰은 다음과 같은 단점도 있습니다.

  • 토큰이 제 3자에 탈취당했을 시 서버에서 핸들링할 방법이 없습니다.

이로 인해 토큰을 만료 기한을 짧게 주어 토큰이 탈취 되더라도 금방 만료될 수 있도록 하는 방법이 도입되었으나 이 경우 사용자의 로그인 상태가 금방 종료되어 사용자가 큰 불편함을 느끼게 되었습니다. 이를 보완하기 위해 사용자를 식별하는 accessToken과 사용기한이 좀 더 긴 refreshToken을 함께 발급해 accessToken이 만료되기 전 refreshToken을 이용해 accessToken을 갱신해주는 방식이 도입되었습니다. 그러나 이 또한 refreshToken이 탈취 당했을 때 이를 이용해 accessToken을 재발급 받을 수 있고 결국 refreshToken이 만료되기까지 서버에서 핸들링할 수 없다는 문제점이 다시 발생합니다.

결국, 완벽한 보완이란 존재할 수 없고 반드시 trade-off가 존재하는 상황에서 프로젝트 상황에 가장 적합한 jwt 토큰 구현 방식이 무엇일지에 대해 고민하게 되었습니다.

💡 jwt 토큰을 어디다 저장해야할까

jwt 토큰 관련 가장 많은 고민을 하게 되는 지점은 jwt 토큰을 어떻게 전달해서 클라이언트에 어떤 방식으로 저장해야할까 입니다. 먼저 http 통신에 있어 대표적으로 고려되는 보안 공격들이 무엇일지 찾아보았습니다.

💻 중간자 공격

클라이언트와 서버사이 중간에 제 3자가 끼어들어 요청 응답 내용을 탈취하거나 통신을 위조 하는 공격입니다. 현 프로젝트는 https를 도입해 중간자 공격을 최소한 방어하고 있습니다. 그러나 https 통신도 중간자 공격에 노출될 위험이 존재합니다.

💻 xss 공격

xss 공격은 사용자가 악의적인 script를 클라이언트 브라우저를 통해 실서버에 삽입해 실행하는 공격입니다.

만약 클라이언트 localStorage에 jwt토큰을 저장한다면 xss공격으로 자바스크립트 코드를 변경해 localStorage에 담긴 값을 불러오거나 불러온 값을 이용해 API콜을 위조할 수 있는 위험이 있습니다.

클라이언트의 쿠키 또한 xss 공격으로 쿠키 안에 담긴 값을 불러오거나 쿠키가 담긴 API 콜을 보내 로그인 위조를 할 수 있습니다.ex.document.cookie와 같은 코드로 가능합니다. 결국 localStorage와 쿠키 모두 xss공격에 취약합니다. 다만 쿠키는 쿠키 옵션으로 최소한의 방어를 할 수 있습니다. httpOnly옵션을 사용하면 http 통신 외에 쿠키에 접근할 수 없습니다. 추가로 secure옵션을 넣어 https 상태에서만 쿠키가 동작할 수 있도록 하면 쿠키값이 노출될 위험이 줄어듭니다.

따라서 개발자는 클라이언트에서 보낸 입력값이 html/script로 읽히지 않도록 따로 처리해주어야합니다. 다행히 React에서는 JSX에 삽입된 script string은 자동으로 escape 처리를 해준다고 합니다.

💻 csrf 공격

공격자가 다른 사이트에서 API콜을 요청해 실행하는 공격입니다. 만약 공격자가 사이트 사용자의 인증 정보를 가지고 있다면 이를 이용해 위조된 API 콜을 보내 로그인한 유저만 가능한 액션들을 실행시킬 수 있습니다. 보통 사용자가 우리 사이트에 접속한 상태로 csrf 공격이 심겨진 특정 사이트를 들어가거나 피싱 이메일을 열어볼 때 공격에 노출됩니다.

쿠기에 accessToken이 저장될 경우 csrf공격에 취약해집니다. 공격자가 쿠기에 담긴 인증정보를 이용해 위조된 API콜을 요청할 수 있기 때문입니다. 이 경우 httpOnly 옵션은 소용이 없습니다.

💡 구현 로직

제가 선택한 구현 방법은 다음과 같습니다.

  • 사용자가 로그인 요청시 로그인 정보가 올바르면 응답으로 accessToken, 쿠키로 refreshToken을 전달한다.
  • 클라이언트는 응답에 담긴 accessToken을 header authorization에 Bearer 형식으로 저장한다.
  • 클라이언트는 새로고침이나 accessToken이 만료될때 refrshToken을 이용해 accessToken을 재발급받는다.

이로 인해 accessToken은 클라이언트 내에 헤더에만 저장되고 새로고침, 페이지 이동 등으로 인해 지속적으로 휘발되어 외부에 노출될 위험을 줄였습니다. 쿠키나 localStorage에 따로 저장하지 않아 위에서 언급된 공격에서도 최대한 보호하고자 했습니다.

반면 refreshToken은 쿠키로 전달했습니다. httpOnly옵션과 secure옵션으로 xss 공격에서 최소한 보호하고자 하였지만 여전히 csrf 공격 위험이 남습니다. 그러나 refreshToken은 accessToken을 발급받는 용도로만 사용됩니다. 만약 공격자가 csrf 공격으로 쿠키의 refreshToken을 이용해 accessToken을 재발급받는 요청을 보내더라도 csrf 공격만으로는 accessToken이 담긴 응답을 확인할 수 없기 때문에 accessToken을 보호할 수 있습니다.

💡 또 다른 방법

jwt 토큰 구현 방법 중 많이 사용되는 방법으로 캐시 서버로 주로 사용되는 redis를 활용하는 방법이 있습니다. 토큰 자체에는 사용자의 정보를 저장하지 않고 빈 토큰과 사용자 식별자를 redis에 저장해 요청이 올때마다 비교하는 식입니다. redis 사용을 깊이 고민해봤지만 다음과 같은 이유로 적용하진 않았습니다.

  • 프로젝트 프로토타입을 만드는 과정이었기 때문에 최소한으로 구현하고 싶었습니다.
  • 이후 프로젝트가 어떤 방향으로 발전될지 모르기 때문에 새로운 기술을 도입하는데 보수적으로 접근해야한다는 생각이 있었습니다.
  • redis는 원 DB와 데이터 sync문제가 발생합니다. 현 시점에서 프로젝트 복잡도를 높이고 싶지 않았습니다.

[참고자료]프론트에서 안전하게 로그인 처리하기 => 사실상 거의 모든 정보를 여기에서 얻었습니다.