반응형

Redis를 이용한 토큰 탈취 대응 시나리오(feat. Refresh Token Rotation)

 

문제 상황

1. 유효기간이 긴 Refresh Token이 탈취된 경우
-> 이 경우는 간단히 Refresh Token Rotation을 떠올릴 수 있다. 하지만 아래의 문제까지 커버가 가능할까?
2. 탈취한 Refresh Token으로 정상 유저보다 먼저 Access Token을 재발급 받는 경우
-> Refresh Token Rotation을 적용 해도, 정상유저보다 먼저 Refresh Token을 재발급 받으면 어떻게 해야하나?
3. 토큰이 탈취된 경우, 한 명의 사용자에 Refresh Token이 여러개 생성되는 경우
-> 여러개 생성된 Refresh Token을 가지고 각각 Access Token을 발급받을 수 있다면 심각한 문제가 생길텐데?
 
 

이 글을 읽기 전, 앞서 작성한 글을 읽고 오길 권장한다.

- JWT 직접 만들어보며 이해하기
- 쿠키와 세션(feat. HTTP의 Connectionless와 Stateless)
- 세션 기반 인증과 토큰 기반 인증(feat. Authentication 과 Authorization)
- 다중 서버 환경에서의 세션 불일치 문제와 해결 방법
- Access Token의 문제점과 Refresh Token
- Redis를 사용하는 이유(feat. Refresh Token)

 
 
앞서 작성한 글을 토대로,
Session 방식과 Token 방식 중, Token 방식을 선택했고, 그중 대표적인 클레임 방식 토큰인 JWT를 사용한다고 가정하자.
지금부터 위에서 제시한 3가지 문제 상황을 차근차근 해결해보도록 하겠다.
 

Access Token과 Refresh Token의 도입

사용자 정보를 인증하기 위해서는 우선 하나의 토큰만 있으면 된다.
클라이언트가 (사용자 정보를 담고 있는) 토큰을 서버에 보내면, 서버는 유효한 사용자인지 검증할 수 있다.

사용자 A는 자신의 토큰(T1)을 사용해서 서버 인증을 요청한다. (출처 : Hoo, I am)

 

Access Token
Access Token은 사용자 인증정보를 담는다. 하지만 탈취의 위험과 혹여나 토큰 내용을 해석할 수 있다는 위험 때문에 최소한의 사용자 정보만 담아야 한다.
토큰 기반 방식의 단점은 토큰이 탈취될 경우에 서버에서 이를 식별하지 못한다.
정상유저A 와 그의 토큰(T1)을 탈취한 해커 H가 있다고 가정하자.
정상유저는 Access Token을 사용해 서버 인증을 거쳐 서비스를 사용한다.
토큰의 Stateless 한 특징으로 서버에선 토큰을 인증할 때 어떤 누가 토큰을 보냈는지 알 수 없다. 

해커 H가 탈취한 토큰(T1)을 서버에 보내도, 서버는 똑같이 인증을 해준다. (출처 : Hoo, I am)

다시말해, Access Token(T1)을 탈취한 해커 H가 T1을 서버에 요청을 보내면, 서버는 정상유저A의 요청이 아니라 해커H의 요청이라는 것을 알 수 없다. 이 경우, 서버는 똑같이 인증을 해줄 것이다.
이러한 문제로인해 Access Token의 유효기간을 짧게 설정한다. (피해를 막을 순 없지만 피해를 방지? 하는 느낌??)

Access Token(T1)의 유효기간을 짧게 설정. (출처 : Hoo, I am)

Access Token의 유효기간이 짧으면, 정상유저A는 Access Token이 만료될 때마다, 매번 로그인 과정을 거쳐 Access Token을 재발급 받아야 한다. 정상유저A 입장에서 굉장히 불편할 것이다. 서비스는 좋은 사용자 경험을 줄 수 없을 것이다.

이 문제를 해결하기 위해 Refresh Token이 도입된다.


Refresh Token

Refresh Token은 Access Token 만료 시 재발급 과정에 사용된다.
Refresh Token은 Access Token 재발급에 사용되어 사용자가 매번 로그인 과정을 거치지 않도록 한다.

Access Token은 사용자의 정보를 담는다고 했는데, 
Refresh Token은 사용자 개인 정보와는 관계 없는 간단한 스트링이나 식별자용 UUID를 담아도 되고, Access Token 처럼 JWT로 사용자 정보를 담아도 된다.
유효기간의 경우 Refresh Token은 Access Token 보다 더 길다.
ex) Access Token 30분, Refresh Token 2주
 
 

Refresh Token을 이용한 Access Token 재발급 과정

 

Access Token이 만료된 경우, Refresh Token으로 사용자 정보를 가져와 Access Token을 재발급한다. (출처 : Hoo, I am)

Access Token이 만료됐을 때, Refresh Token이 어떻게 이용되는지 살펴보자.
우선, Access Token은 사용자 정보를 담고있다. Access Token을 재발급 한다는 것은, 토큰에 사용자 정보를 넣어준다는 뜻이다.
즉, 사용자 정보가 준비되어야 한다. Refresh Token은 서버에 전송되어 사용자 정보를 가져올 Key 역할을 한다. 사용자 정보가 저장되어 있는 RDB or Redis에서 사용자 정보를 꺼내올 수 있다. 그후, 그 정보를 매개로하여 Access Token을 재발급 한다.
 

Access Token과 Refresh Token 둘다 탈취 된 경우

앞서 Aceess Token이 탈취될 경우를 고려하여 유효기간을 짧게 설정한다고 했다.

그럼 Aceess Token과 Refresh Token 둘다 탈취된 상황을 가정해보자.
앞서 제시한 1. 유효기간이 긴 Refresh Token이 탈취된 경우 가 발생한 경우이다.

해커가 Refresh Token을 이용하여 Access Token을 갱신 한다. 피해에 노출될 것이다. (출처 : Hoo, I am)

Access Token의 유효기간이 짧아도, Refresh Token 의 유효기간이 길기 때문에,
해커는 탈취한 Refresh Token을 이용하여 Access Token을 갱신할 수 있을 것이다.
게다가 Refresh Token은 유효기간이 길기 때문에, 한번 탈취되면 만료시까지 해커가 계속 사용할 수 있다.
심지어, 토큰 기반 인증의 Stateless 한 특징 때문에 서버는 해커에게 토큰이 탈취된 지도 모른 채 계속 인증을 허가해 줄 것이다.
이런 문제를 해결하기 위해 Refresh Token Rotation 기법을 도입한다.
 

Refresh Token Rotation 도입

정상유저A가 해커보다 먼저 재발급 시도(출처 : Hoo, I am)

 

정상유저A가 수행한 재발급에 의해 재발급 실패(출처 : Hoo, I am)

기존에는 Refresh Token을 이용하여 Access Token만 재발급 했지만,
Refresh Token Rotation을 도입하여 Access Token과 Refresh Token을 함께 재발급한다.
서버측에서는 Refresh Access Token 재발급 시 Redis나 RDB에 저장된 Refresh Token도 함께 업데이트 한다.
그러면, 정상유저A의 Access Token과 Refresh Token을 해커H가 탈취했다하더라도,
해커 H보다 정상유저A가 더 먼저 Refresh Token을 이용하여 Access Token을 재발급을 하면, Refresh Token도 함께 재발급 되기 때문에, 해커H가 탈취해서 가지고 있는 Refresh Token과 서버에 저장된 Refresh Token이 달라진다.
그러면, 해커H가 탈취한 Refresh Token은 사용할 수 없는 상태가 된다.
문제가 해결된 것 같지만 전혀 아니다.
앞서 제시한 문제 2. 탈취한 Refresh Token으로 정상 유저보다 먼저 Access Token을 재발급 받는 경우가 발생한다.
탈취한 Refresh Token의 유효기간 내에 정상유저A보다 해커H가 더 먼저 RefreshToken을 이용하여 재발급을 시도하면, 
서버측에서는 문제상황임을 인지할 수 없고, 정상 처리를 하여 해커H에게 Access Token과 Refresh Token 재발급을 해줄 것이다.
그러면, 서버에는 해커H에게 새로 재발급한 Refresh Token이 저장될 것이다.
그러면, 정상유저A는 Refresh Token을 이용하여 Access Token을 재발급 하려고 하면, 서버에 저장된 Refresh Token과 자신이 가진 Refresh Token이 다르므로, 재발급이 거절되고, 로그인을 다시 해야할 것이다.
그러면 만약 Refresh Token을 저장하는데에 Redis를 사용하는 서비스이고, 
Redis에 Refresh Token을 저장할 때, {Key : Value = Refresh Token : UserPK} 형태로 저장을 한다면,
이번에 정상유저A가 새로 로그인하여 생겨난 {Refresh Token(1) : 정상유저A의PK} 데이터가 Redis에 저장될 것이고,
앞서 해커H가 탈취한 토큰으로 재발급받은 Refresh Token정보도 {Refresh Token(2) : 정상유저A의PK} 형태로 저장 될 것이다.
문제는 여기서 발생된다.  

Redis에 Refresh Token을 저장할 때, {Key : Value = Refresh Token : User정보} 형태로 저장을 한다면,
위와 같은 상황이 발생하면, 정상유저A에 대한 Refresh Token이 여러개 저장되게 된다.
즉, 앞서 제시한 문제 3. 토큰이 탈취된 경우, 한 명의 사용자에 Refresh Token이 여러개 생성되는 경우 가 발생하는 것이다.
이 경우, 별다른 조치를 취하지 않는 한, 해커H가 탈취한 Refresh Token을 유효기간이 끝나기 전마다 계속 갱신해서 악용한다면, 이를 막을 방법이 없다.
남은 두가지 문제를 해결해보자.
 

Redis 저장 방식 변경

Refresh Token은 단순 토큰이 아닌 사용자 정보를 담은 JWT 형태를 사용했고, 이를 저장하기 위해 Redis 를 사용한다고 가정하자.
기존에는 Redis에 Refresh Token을 저장할 때, Key로 Refresh Token을 사용하고, Value에 사용자 정보를 저장했다.
그러면, Access Token을 재발급 받아야 할 때, Refresh Token으로 사용자 정보를 조회하고, 그것을 이용하여 Access Token을 재발급 받는 프로세스였다.
이 구조를 바꿔보자.
{Key : Value = UserPK : Refresh Token} 형태로 저장하자. 이것이 핵심이다.
기존과 반대로 저장한 이유는
UserPK에 대응되는 Refresh Token을 1개로 제한하기 위함이다.
이것이 왜 문제해결의 핵심일까?
우선, 해커H가 Access Token과 Refresh Token을 모두 탈취 한 상태이고,
재발급 과정도 정상유저A보다 먼저 수행한 상황이라고 가정하자.

특정유저에 대해서 Redis에 저장된 Refresh Token과 갱신의 시도하는데 이용한 Refresh Token이 매칭되지 않으면 악의 적인 침투가 있었음을 인지(출처 : Hoo, I am)

이때 정상유저A가 Refresh Token을 이용하여 Access Token과 Refresh Token을 재발급받는 과정을 살펴보면,

  1. 정상유저A의 Refresh Token에서 UserPK 정보를 꺼낸다. (UserPK는 UserEmail을 사용한다고 가정)
  2. UserPK를 Key로  Redis를 조회하면, 이에 대응되는 Value에 저장된 Refresh Token이 리턴된다.
  3. Redis에서 조회된 Refresh Token은 해커에 의해 먼저 재발급된 Refresh Token이므로, 정상유저A가 가지고 있던 Refresh Token과 상이한 토큰이다. 즉, 매칭되지 않는다.
  4. 이렇게 특정유저에 대해서 Redis에 저장된 Refresh Token과 갱신의 시도하는데 이용한 Refresh Token이 매칭되지 않으면 악의 적인 침투가 있었음을 인지.
  5. Redis에서 해당 유저와 대응되는 UserPK : Refresh Token 데이터를 삭제하고, 재로그인 하도록 클라이언트 리턴을 한다.

 
이렇게 Redis에 저장 하는 방식을 변경하면,

2. 탈취한 Refresh Token으로 정상 유저보다 먼저 Access Token을 재발급 받는 경우
3. 토큰이 탈취된 경우, 한 명의 사용자에 Refresh Token이 여러개 생성되는 경우
이어지는 문제를 해결할 수 있음을 확인했다.
지금까지의 내용을 정리하도록 하겠다.
 

정리

해결하고자 하는 3가지 문제상황은 다음과 같았다.
1. 유효기간이 긴 Refresh Token이 탈취된 경우
-> 이 경우는 간단히 Refresh Token Rotation을 떠올릴 수 있다. 하지만 아래의 문제까지 커버가 가능할까?
2. 탈취한 Refresh Token으로 정상 유저보다 먼저 Access Token을 재발급 받는 경우
-> Refresh Token Rotation을 적용 해도, 정상유저보다 먼저 Refresh Token을 재발급 받으면 어떻게 해야하나?
3. 토큰이 탈취된 경우, 한 명의 사용자에 Refresh Token이 여러개 생성되는 경우
-> 여러개 생성된 Refresh Token을 가지고 각각 Access Token을 발급받을 수 있다면 심각한 문제가 생길텐데?
 

  1. 1번문제의 경우 Refresh Token Rotation(RTR) 을 적용하여 문제를 해결했다.
    Refresh Token이 탈취 되더라도, Access Token을 재발급 받을 때 Refresh Token도 함께 재발급 되도록 하여 기존 Refresh Token을 무효화 했다.
    하지만,  이어지는 2번3번 문제를 해결할 수 없었다.
  2. 하지만 Refresh Token Rotation만 적용한 경우, 정상유저가 해커보다 먼저 탈취당한 Refresh Token을 이용하여 토큰 갱신을 하게되면 문제가 없는 해결방법이지만, 갱신을 해커가 탈취한 Refresh Token으로 정상유저보다 먼저 재발급을 받아버리면, 오히려 정상 유저의 Refresh Token이 무효화 되는 문제가 남아있다.

    이를 해결하기위해
    Redis에 {Key : Value = UserPK : Refresh Token} 형태로 저장하도록 저장혁식을 변경하여 UserPk당 토큰을 1개로 제한하였고, 토큰 갱신 시도시에 토큰이 매칭되지 않으면 악의적인 침투를 인지하도록 하여 대응하도록 했다.

    이방법을 적용하므로 인해 2번, 3번 문제를 동시에 해결할 수 있었다.

 
 

추가로 고민해볼 만한 포인트

앞서 제시한 3가지 문제상황을 Refresh Token Rotation과 Redis에서의 데이터 저장 방식을 이용해서 잘 해결했다.
하지만, 완벽히 문제를 해결한 것은 아니다.

만약, 정상유저A의 Refresh Token을 해커에게 탈취당했는데, 정상유저A가 그 이후 오랫동안 서비스에 접속하지 않는다면, 
서버는 지속적으로 해커가 탈취한 Refresh Token을 통해 해커에게 지속적으로 재발급 할 것이고, Redis에서도 토큰 비교 과정에서 악의적인 침투라는 것을 인지할 방법이 없다. 
이것은, Refresh Token Rotation + Redis 의 문제라기보다는, 토큰 기반 인증방식이 가지는 필연적인 문제인 것 같다.
결국, 클라이언트 쪽에서도 토큰을 탈취당하지 않도록 잘 관리하는 것이 중요하다.(더 좋은 방법이 있을수도..?)

 

[참고]
https://betterprogramming.pub/should-we-store-tokens-in-db-af30212b7f22

반응형