포스트

08.보안을 위한 JWT

토큰 사용 추적

  1. 로그인 성공 : 서버에서 클라이언트로 JWT 발급
  2. 권한이 필요한 모든 요청 : 클라이언트에서 서버로 JWT 전송

  • 권한이 필요한 요청은 서비스에서 많이 발생한다(회원 CRUD, 게시글/댓글, 상품 주문 등등..)
  • 따라서 JWT는 매시간 수많은 요청을 위해 클라이언트의 JS 코드로 HTTP 통신을 통해 서버로 전달된다
  • 해커는 클라이언트 측에서 XSS를 이용하거나 HTTP 통신을 가로채서 토큰을 훔칠 수 있다.
  • 토큰은 발급되면 기간이 만료되기 전에는 토큰을 가진 사람이면 누구나 이용할 수 있으므로 보안에 위험하다.

보안

다중 토큰 : Refresh 토큰과 생명 주기
  • Access 토큰과 Refresh 토큰의 등장
  • 자주 사용되는 Access Token(ATK)는 생명주기를 짧게(약 10분)으로 하고, Refresh Token(RTK)는 생명주기를 길게(24시간 이상)으로 설정
  • ATK이 만료되면 RTK로 토큰을 재발급한다
  • ATK만으로 하게되면 매번 서비스 요청마다 토큰을 발급받기 위해 매번 로그인을 진행해야 할 수도 있다. 그래서 생명주기가 긴 RTK도 발급하는 것이다.

  1. 로그인 성공 시 생명주기와 활용도가 다른 토큰을 2개 발급 ATK, RTK
    • Access토큰 : 권한이 필요한 모든 요청 헤더에 사용될 JWT로 탈취 위험을 낮추기 위해 10분정도의 짧은 생명주기를 가진다
    • Refresh토큰 : Access 토큰이 만료되었을 때 재발급 받기 위한 용도로만 사용한다. 24시간 이상의 긴 생명주기를 가진다
  2. 권한이 필요한 모든 요청
    • ATK을 이용해 권한 요청
    • ATK만 사용하여 요청하기 때문에 RTK은 호출 및 전송 빈도가 낮다
  3. 권한이 알맞다는 가정하에 2가지 상황
    • 데이터 응답
    • 토큰 만료 응답
  4. 토큰이 만료된 경우 RTK로 ATK 재발급
    • ATK가 만료되었다는 요청이 돌아왔을 경우 프론트엔드 로직에 의해 1에서 발급 받은 RTK를 가지고 서버의 특정 경로(RTK를 받는 경로)에 요청을 보내어 ATK 재발급
  5. 서버측에서는 RTK를 검증 후 ATK 재발급 진행
다중 토큰 구현 포인트
  1. 로그인이 완료되면 successHandler에서 ATK 와 RTK 를 발급해 응답한다
    • 각 토큰은 각기 다른 생명주기, payload 정보를 가진다
      • JWT는 Header, Payload, Signature 구조를 가지고 있고,
      • Header는 JWT임을 명시하고, 사용된 암호화 알고리즘을 넣는다.
      • Payload는 정보를 담고 있다
      • Signature는 암호화 알고리즘(BASE64(Header) + BASE64(Payload) + 암호화키) 의 정보를 가진다
  2. ATK 요청을 검증하는 JWTFilter에서 ATK이 만료된 경우는 프론트 개발자와 협의된 상태 코드와 메시지로 응답한다.
  3. 프론트측 API 클라이언트 요청 시 ATK 만료 요청이 오면 예외문을 통해 RTK를 서버측으로 전송하여 ATK 재발급 로직을 수행한다. 기존 ATK는 제거한다.
  4. 서버측에서는 RTK를 받을 엔드포인트(컨트롤러)를 구성하여 RTK를 검증하고 ATK를 응답한다.
RTK가 탈취되는 경우
  • 단일 → 다중 토큰으로 전환하며 자주 사용되는 ATK이 탈취되더라도 생명주기가 짧아 피해 확률이 줄었다.
  • 하지만 RTK 또한 사용되는 빈도만 적을 뿐 탈취될 수 있는 확률이 있기 때문에 RTK에 대한 보호 방법도 필요하다
    1. ATK / RTK 의 저장위치 고려
  • 로컬/세션 스토리지 및 쿠키에 따라 XSS, CSRF 공격의 여부가 결정된다
  • 따라서 각 토큰 사용처에 알맞은 저장소를 설정한다
    1. RTK Rotate
  • ATK을 갱신하기 위한 RTK 요청 시 서버측에서 RTK도 재발급을 진행하여 한 번 사용한 RTK은 재사용하지 못하도록 한다
ATK, RTK 저장 위치
  • 클라이언트에서 발급 받은 JWT를 저장하기 위해 로컬 스토리지와 쿠키에 대해 많은 고려를 해야 한다.
  • 각 스토리지의 취약점
    • 로컬스토리지 : XSS 공격에 취약하다. ATK를 저장
    • httpOnly 쿠키 : CSRF 공격에 취약하다. RTK를 저장
    • 무조건 이렇게 하지 않아도 된다
  • 고려 사항
    • JWT의 탈취는 보통 XSS 공격으로 로컬 스토리지에 저장된 JWT를 가져간다.
    • 그럼 쿠키 방식으로 저장하면 안전하지 않을까라는 의문이 들지만, 쿠키 방식은 CSRF 공격에 취약하다
  • ATK
    • ATK는 주로 로컬 스토리지에 저장된다
    • 짧은 생명주기로 탈취에서 사용까지 기간이 매우 짧고, 에디터 및 업로더에서 XSS를 방어하는 로직을 작성하여 최대한 보호 할 수 있다.
    • 하지만 CSRF 공격의 경우 클릭 한 번으로 단시간에 요청이 진행되기 때문에 방어가 어렵다
    • 따라서 권한이 필요한 모든 경로에 사용되기 때문에 CSRF 공격의 위험보다는 XSS 공격을 받는게 더 나은 선택일 수 있다 -RTK
    • RTK는 주로 쿠키에 저장한다
    • 쿠키는 XSS 공격을 받을 수 있지만 httpOnly를 설정하면 완벽히 방어가 가능하다
    • CSRF 공격에 대해서는 생각해보면, RTK의 사용처는 토큰 재발급 경로이다.
    • CSRF는 ATK가 접근하는 회원 정보 수정, 게시글 CRUD에 취약하지만 토큰 재발급 경로에서는 크게 피해를 입힐 만한 로직이 없다.
RTK Rotate
  • 저장소의 특징에 맞게 토큰을 저장하고 보호하여도, 탈취당할 가능성이 있다.
  • 따라서 생명주기가 긴 RTK에 대한 추가적인 방어 조치가 필요하다
  • ATK가 만료되어 RTK을 가지고 서버 특정 엔드포인트에 재발급을 진행하면 RTK 또한 재발급하여 프론트측으로 응답하는 방식이 Refresh Rotate 이다
로그아웃과 Refresh 토큰 주도권
  • 로그아웃을 구현하면 프론트측에서는 ATK와 RTK를 제거한다.
  • 그렇게 하면 프론트에서 보낸 JWT가 없기 때문에 로그아웃이 되었다고 생각할 수 있다.
  • 하지만 JWT가 이미 탈취된 상태라면 해커의 요청이 수행될 수 있다.
  • 위와 같은 문제가 발생하는 이유는 단순하게 JWT를 발급해주는 순간 서버측의 주도권이 없기 때문이다.
  • 왜냐하면 세션 방식처럼 상태를 STATE하게 관리하여 주도권이 서버측에 있지 않기 때문이다.
  • 또한, 로그아웃 뿐만 아니라 JWT가 탈취되었을 때는 서브 측에 주도권이 없기 때문에 피해를 막을 방법이 생명주기가 끝나기를 기다리는 수 밖에 없다

  • 방어 방법
  • 위 문제의 해결법은 생명주기가 긴 RTK의 발급과 함께 서버측 저장소에도 저장하여 요청이 올 때마다 저장소에 존재하는지 확인하는 방법이 있다. 이 방법으로 서버측에서 주도권을 가질 수 있다
  • 만약 로그아웃을 진행하거나 탈취에 의해 피해가 진행되는 경우 서버측 저장소에서 해당 JWT를 삭제하여 피해를 방어할 수 있다.
  • 이것을 ‘Refresh 토큰 블랙리스팅’ 이라고 한다
로그인 시 메일 알림
  • 네이버 서비스를 이용하다 보면 평소에 사용하지 않던 IP나 브라우저에서 접근할 경우 사용자의 계정으로 메일 알림이 발생한다
  • 이때 내가 아닌 경우 ‘아니오’를 클릭하게 되면 서버측 토큰 저장소에서 해당 유저에 대한 Refresh 토큰을 모두 제거하여 앞으로의 인증을 막을 수 있다
참고 사이트

개발자 유미