JWT에서 Refresh Token은 왜 필요한가?

park2348190·2021년 11월 11일
43

Web Basics

목록 보기
3/4

서론

개인 프로젝트 중 JWT를 사용하는 SimpleTodoList 에서는 회원가입 후 로그인 시 아래처럼 JWT를 발급해준다.

=== REQUEST
{
    "username": "testusername",
    "password": "testpassword"
}

=== RESPONSE
{
    "success": true,
    "result": {
        "id": 1,
        "username": "testusername",
        "alias": "Kwonkyu",
        "password": "ENCRYPTED",
        "locked": false
    },
    "message": "eyJhbGciOiJI..."
}

이 토큰은 애플리케이션 전반에서 사용자를 인증하는데 사용된다. 기존의 세션과는 달리 다양한 플랫폼에서 토큰만으로 인증할 수 있다는 장점이 있어 유용하게 사용하고 있다.

그런데 이 JWT를 활용한 기능 개발에 몰두하느라 가장 중요한 점을 잊고 있었다. 즉 이 토큰 자체가 HTTPS를 사용하지 않는 환경이거나 기타 보안 취약점으로 인해 노출되었을 때 어떻게 이 문제를 해결할 것인가? 라는 점이다.

현재 애플리케이션에서는 토큰의 유효 기간을 하루로 설정하고 있다. 그렇게 길지는 않은 시간이지만 만약 이 토큰이 탈취당한다면 하루동안 서버는 탈취당한 사람(피해자)과 탈취한 사람(공격자)을 구분할 수 없기 때문에 문제가 될 수 있다.

인증 토큰을 탈취당했다는 것 자체가 문제지만 일단 이런 문제가 발생했을 때 어떻게 피해를 최소화할 수 있을까? 이 때 사용할 수 있는 것이 Refresh Token이다. 현재 프로젝트에는 이 Refresh Token 기능이 아직 구현되지 않았기 때문에 추후 구현을 위해 이에 대해 좀 더 조사해보았다.

본론

JWT 구조

일단 JWT는 위키피디아, Auth0, jwt.io 등 다양한 사이트에서도 이미 잘 설명하고 있지만 비밀키를 이용하여 서명된 JSON 형태의 데이터다. 여기에 사용자 인증 정보를 넣어서 토큰을 발급해주면 추후 인증이 필요한 리소스에 접근 시 사용자가 서버에 토큰을 포함해서 전송하여 서버측에서는 복잡한 인증 과정 없이 토큰만으로 사용자를 인증 및 인가할 수 있는 것이다.

JWT는 헤더, 페이로드, 서명 세 가지 정보를 base64로 인코딩한 값을 콤마('.')를 사이에 두고 이어붙인 형태로 생성된다.

  • 헤더: JWT 서명에 사용된 알고리즘을 담는다.
  • 페이로드: 토큰에 담긴 주체(Subject), 만료일(exp), 생성자(iss) 등을 담는다.
  • 시그니처: 헤더와 페이로드를 각각 base64로 인코딩한 후 콤마로 이어붙인다. 그리고 이를 헤더에 명시된 알고리즘으로 암호화한 값을 담는다.

이때 비대칭키 암호화 방식을 사용하기 때문에 서버측에서는 이 토큰을 받아서 시그니처를 복호화하여 디코딩하는 방식으로 토큰의 유효성을 검증할 수 있다.

Access Token, Refresh Token

위의 방식으로 받은 토큰은 언급했듯이 서버측 리소스에 접근할 때 클라이언트 본인을 인증할 수 있는 액세스 토큰으로 동작한다. 그런데 이 JWT는 Stateless한 방식이기 때문에 서버측에서는 이 토큰을 갖고 있는 클라이언트가 정말 클라이언트 본인이 맞는지 확인할 수 없다는 문제점이 있다.

그래서 이에 대한 보안 대책으로 리프레쉬 토큰이라는 추가적인 토큰을 활용할 수 있다. 이 리프레쉬 토큰은 사용자 인증이 아닌 새로운 액세스 토큰을 생성하는 용도로만 사용된다. 그러면 왜 굳이 별도의 토큰을 두고 새로운 액세스 토큰을 발급받도록 한 것일까? 이는 위의 JWT 유출 문제를 다음처럼 해결하기 위한 것이다.

  • Access Token의 유효 기간을 짧게 설정한다.
  • Refresh Token의 유효 기간은 길게 설정한다.
  • 사용자는 Access Token과 Refresh Token을 둘 다 서버에 전송하여 전자로 인증하고 만료됐을 시 후자로 새로운 Access Token을 발급받는다.
  • 공격자는 Access Token을 탈취하더라도 짧은 유효 기간이 지나면 사용할 수 없다.
  • 정상적인 클라이언트는 유효 기간이 지나더라도 Refresh Token을 사용하여 새로운 Access Token을 생성, 사용할 수 있음.

즉 OTP 인증처럼 짧은 시간 동안에만 사용할 수 있도록 하고 주기적으로 재발급받도록 하여 토큰이 유출되더라도 그 피해를 최소화한다는 방식이다. 단순히 Access Token 만으로는 일일히 IP 주소의 위치를 파악해서 비교하는 게 아닌 이상 토큰의 탈취를 검증하기 어렵기 때문에 토큰이 탈취되더라도 그 피해(attack window)를 줄이기 위해 토큰의 사용 시간 자체를 줄이는 것이다.

하지만 그렇다면 정상적인 클라이언트도 짧은 주기마다 다시 로그인해서 Access Token을 발급받아야 한다는 단점이 있다. 그래서 여기서 유효 기간이 긴 Request Token을 사용하는데 정상적인 사용자는 Access Token이 만료됐다면 서버측에 Request Token을 전송하여 다시 로그인할 필요 없이 Access Token을 발급받을 수 있다. 당연히 이 Request Token이 없는 공격자는 다시 토큰을 발급받을 수 없기 때문에 보안 측면에서 좀 더 안전하다고 할 수 있다.

또 같은 사용자가 여러 디바이스(스마트폰, 태블릿, PC 등)에서 접근하는 경우 각 디바이스 타입에 맞는 Access Token, Refresh Token 쌍이 필요할 것이다.

Refresh Token의 탈취

그런데 이 Refresh Token 자체가 탈취당한다면 어떻게 할까? 공격자는 이 토큰의 유효 기간만큼 다시 Access Token을 생성해서 다시 정상적인 사용자인 척 위장할 수 있다. 그렇기 때문에 여기서는 서버측의 검증 로직이 필요한데 스택오버플로우의 답변을 보면 다음과 같은 방법을 제안하고 있다.

  • 데이터베이스에 각 사용자에 1대1로 맵핑되는 Access Token, Refresh Token 쌍을 저장한다.
  • 정상적인 사용자는 기존의 Access Token으로 접근하며 서버측에서는 데이터베이스에 저장된 Access Token과 비교하여 검증한다.
  • 공격자는 탈취한 Refresh Token으로 새로 Access Token을 생성한다. 그리고 서버측에 전송하면 서버는 데이터베이스에 저장된 Access Token과 공격자에게 받은 Access Token이 다른 것을 확인한다.
  • 만약 데이터베이스에 저장된 토큰이 아직 만료되지 않은 경우, 즉 굳이 Access Token을 새로 생성할 이유가 없는 경우 서버는 Refresh Token이 탈취당했다고 가정하고 두 토큰을 모두 만료시킨다.
  • 이 경우 정상적인 사용자는 자신의 토큰도 만료됐으니 다시 로그인해야 한다. 하지만 공격자의 토큰 역시 만료됐기 때문에 공격자는 정상적인 사용자의 리소스에 접근할 수 없다.

중요한 것은 발급된 토큰 자체는 그냥 그 JWT 문자열 자체로 존재하는 것이기 때문에 클라이언트나 서버측에서 전역적으로 만료시킬 수 있는 개체가 아니다. 그렇기 때문에 토큰의 유효 기간이 지나기 전까지는 만료된 토큰을 NoSQL 같은 데이터베이스에 저장하여 관리할 필요가 있다.

만약 공격자가 Refresh Token을 탈취해서 정상적인 사용자가 Access Token을 다시 발급받기 전에 자기가 먼저 Access Token을 생성한다면 어떻게 될까? 이 경우에도 Access Token의 충돌이 일어나기 때문에 서버측에서는 두 토큰을 모두 폐기(만료 대신 폐기가 어울리는 용어인 것 같다)해야 할 것이다. 그래서 ietf 문서에서는 아예 Refresh Token도 Access Token과 같은 유효 기간을 가지도록 하여 사용자가 한 번 Refresh Token으로 Access Token을 발급받았으면 Refresh Token도 다시 발급받도록 하는 것을 권장하고 있다.

그럼 만약에 공격자가 Access Token, Refresh Token을 둘 다 탈취한다면 어떻게 할까? 이 때는 방법이 없다. 프론트엔드나 백엔드 로직을 강화하여 토큰이 유출되지 않도록 보완하는 수 밖에 없을 것이다. 이를 답변에서는 다음처럼 표현했다.

but then again there is nothing like 100% security.

이런 토큰을 읽고 쓰는 데이터베이스는 빠른 속도가 필요하고 키-값 쌍으로 다루는 편이 적절하다고 생각하기 때문에 이 부분에 NoSQL 데이터베이스를 적용해 볼 수 있을 것 같다.

토큰의 저장 장소

서버에서는 NoSQL이나 기타 데이터베이스에 저장할 수 있다. 그럼 클라이언트에서는 어디에 저장할 수 있을까? 브라우저 환경의 경우 흔히 생각하는 방법은 쿠키, 로컬 스토리지 등 다양한 곳이 있지만 스택오버플로우에서는 http-only 속성이 부여된 쿠키에 저장하는 것을 권장하고 있다.

왜냐면 해당 속성이 부여된 쿠키는 자바스크립트 환경에서 접근할 수 없기 때문이다. 그래서 XSS나 CSRF가 발생하더라도 토큰이 누출되지 않는다. 일반 쿠키나 브라우저의 로컬 스토리지는 자바스크립트로 자유롭게 접근할 수 있기 때문에 보안 측면에서는 권장되지 않는다.

결론

JWT로 인증하는 것만 생각하고 토큰 자체가 탈취될 경우 발생할 수 있는 문제는 생각하지 않았다는 점이 부끄럽다. 관련 서비스를 적용하는 것을 최우선으로 하고 이 김에 NoSQL도 적용해보려고 한다.

참고

Is a Refresh Token really necessary when using JWT token authentication?
ietf

profile
YUKI.N > READY?

13개의 댓글

comment-user-thumbnail
2022년 1월 19일

좋은 글 감사합니다.
해당 글에 나온 방식대로 액세스 토큰과 리프레시 토큰을 서버에서 관리하면 더욱 안전한 인증이 될 수 있다고 생각합니다. 다만, JWT의 무상태성이라는 특징을 옅게 만들어서 세션과 큰 차이가 없어 보이는데 이런 상황에서도 JWT를 사용하는 이유가 있을까요?

1개의 답글
comment-user-thumbnail
2022년 5월 22일

안녕하세요! 글 잘 읽었습니다~
궁금한 점이 있는데 매번 클라이언트측에서 액세스 토큰과 리프레시 토큰을 같이 보낼 경우, 공격자가 이를 따로 탈취하는 경우가 있나요?
공격자가 탈취하는 방식은 어떤 경우가 있을까요?

1개의 답글
comment-user-thumbnail
2022년 6월 29일

JWT관련해서 accesToken과 refreshToken에 대해서 여러 고민 많이 했었는데 도움 많이 되었습니다.
감사합니다.

1개의 답글
comment-user-thumbnail
2022년 7월 19일

access_token + refresh_token 모두 공격자로 부터 탈취당하면요? 답없겠죠?
제가 개발할때는 access_token 하나만 사용했는데 refresh_token 은 스펙상 필수 인가요?
여러 강좌를 봐도 access_token만 생성하는것만 하는것도있고 access_token + refresh_token 2개 모두 생성하는 것도있고 ,차이점을 모르겠네요.
글에서 쓴것처럼 인증용(access_token) , 오로지 재발급(refresh_token) 으로만 사용되는건가요?

1개의 답글
comment-user-thumbnail
2022년 8월 3일

좋은 글 감사합니다.
제가 잘 이해하고 있는지 모르겠는데 Access Token이 만료되기 전 사용자가 다른 컴퓨터로 접속하거나 모바일로 접속하는 경우 Access Token을 새로 만들어주는 것 같은데 그렇게 되면 기존과 새로 접속한 곳 둘 다 접속이 안 되는 건가요?
Access토큰에는 JWT로 긴문자열로 되어있는 거 같은데 Refresh 토큰에 실리는 자료는 어떤 내용이 담겨 DB와 어떤 식으로 비교하는지 알 수 있을까요?

답글 달기