무지를 아는 것이 곧 앎의 시작

Web

내편 리프레시 토큰 도입기

Alex96 2022. 10. 13. 00:34

우아한테크코스 4기 과정에서 팀 프로젝트로 내편이라는 서비스를 개발하고 있다.

모임을 만들고 그 모임의 구성원들끼리 롤링페이퍼를 작성할 수 있는 서비스다.

해당 서비스에선 로그인 구현을 1시간 유효기간의 Access Token을 클라이언트에 발급해서 구현했는데,

직접 사용해보고 우테코 내의 다른 크루들의 피드백을 받았을 때 로그인이 너무 자주 풀려서 불편하다는 피드백을 받았다. 그래서 리프레시 토큰 도입을 고민했고 이를 적용한 내용을 기록해볼까 한다.

리프레시 토큰 도입을 고민하게 된 이유

  1. 현재 accessToken 유효시간은 1시간인데, 이 시간이 지나면 사용자는 로그인이 풀리게 된다.
    1. 너무 자주 로그인이 풀리고, 매번 로그인을 다시 하기 번거롭기 때문에 개선해야 한다.
  2. 그럼 accessToken 유효기간 늘리면 안돼?
    1. 안된다. 보안상의 이유. 토큰 방식을 사용할 경우 사용자의 상태를 서버에 저장하지 않지만, 토큰이 탈취됐을 경우 토큰을 즉시 무효화시킬 수 없다. 그래서 만료 시간을 짧게 두어야 한다.
  3. 매 요청마다 accessToken 갱신하면 안돼?
    1. 서버 비용 부담이 너무 크다. 안좋음.
  4. 그런 이유로 refreshToken을 도입해서 서버의 비용과 안전성 사이의 균형을 맞춘다.

리프레시 토큰 관리 전략에 대한 고민

내편 서비스만의 리프레시 토큰 관리 전략을 세우기 위해 몇몇 레퍼런스를 참조해봤다.

대표적으로 도움이 되었던 것은 카카오 OAuth, 구글 OAuth 문서를 보고 여기선 리프레시 토큰을 어떻게 관리할까? 했던 고민


우리 OAuth 썼던 카카오 로그인 API - (갱신시 리프레시 토큰 기간이 짧게 남았으면 같이 갱신, 아니면 엑세스 토큰만 갱신)

image

)

image

)

image

액세스 토큰과 리프레시 토큰을 갱신합니다. 
JavaScript SDK 사용 시에도 보안 정책으로 인해 REST API로 액세스 토큰을 갱신합니다. 
리프레시 토큰 값과 필수 파라미터를 담아 POST로 요청합니다.

응답은 토큰 받기와 마찬가지로 JSON 객체로 전달됩니다. 
응답 중 refresh_token 값은 요청 시 
사용된 리프레시 토큰의 만료 시간이 1개월 미만으로 남았을 때만 갱신되어 전달됩니다.
따라서 refresh_token과 refresh_token_expires_in은 결과 값에 포함되지
않을 수 있다는 점을 응답 처리 시 유의해야 합니다.

설명을 보면 여긴 리프레시 토큰의 유효기간이 짧게(1달 미만) 남았을 경우 엑세스 토큰 갱신시에 리프레시 토큰도 갱신해주고 있다.


구글 OAuth 로그인 API

image

여긴 심플하게 리프레시 토큰으로 엑세스 토큰만 새로 딱 발급한다.

그리고 서버에서 발급 가능한 리프레시 토큰 갯수에 제한이 있다.

발급되는 갱신 토큰 수에는 제한이 있습니다. 
하나는 클라이언트/사용자 조합당 한 개이고, 다른 모든 클라이언트는 사용자당 한 개입니다. 
갱신 토큰을 장기 스토리지에 저장하고 유효한 한 계속 사용하는 것이 좋습니다.
애플리케이션에서 갱신 토큰을 너무 많이 요청하는 경우 이러한 한도에 도달하여 
이전 토큰이 작동하지 않을 수 있습니다.

발급하는 리프레시 토큰을 DB에 저장하다 보니까 한 사용자당 발급된 토큰 갯수를 한정해놓고 넘어가면 가장 오래된 토큰을 무효화시키는 것 같다.

그리고 여기는 로그아웃시 서버의 리프레시 토큰을 지워서 자원을 절약할 수 있게 하기 위함인지 이런 API도 있었다.

image


이 외에도 구글링이나 다른 팀 프로젝트 레포를 살펴보며 레퍼런스를 모아서 회의를 통해 내편 서비스의 리프레시 토큰 관리 전략을 정했다.

내편 리프레시 토큰 관리 전략

  • 기존 Access Token 만료 기간을 1시간에서 30분으로 줄인다.
  • Refresh Token 만료 기간은 7일로 한다.
  • 발급한 Refresh Token은 서버에 저장한다.
  • Refresh Token은 UUID이다.
  • Refresh Token을 사용해서 새 Access Token을 요청시 Refresh Token의 만료 기간이 2일 이하로 남았다면 만료 기간을 서버에서 7일로 갱신해준다. (자주 접속시 무한 로그인 가능)
  • Refresh Token의 탈취에 대응하기 위해 특정 Refresh Token무효화 및 신고를 받은 사용자의 전체 Refresh Token 무효화 기능을 제공한다.
  • Refresh Token은 계정당 3개까지만 서버에서 관리하고 3개가 이미 관리되고 있을 시 로그인 요청이 오면 가장 오래된 리프레시 토큰을 만료시킨 후 새 리프레시 토큰을 발급한다.

전략의 이유들

왜 Refresh Token은 Access Token처럼 JWT가 아닌 UUID일까?

JWT의 장점은 토큰 자체에 정보를 저장할 수 있어서 서버의 자원을 사용하지 않는다는 데에 있다.

하지만, 서버에서 토큰을 관리하려면 토큰을 서버에 저장해야하고 이는 곧 서버의 자원을 사용하는 것이 된다.

즉 Refresh Token을 JWT로 해봤자 JWT의 장점을 이용하지 못할 뿐 더러 괜히 JWT를 원하는 데이터로 파싱하는 비용만 낭비되는 꼴이 된다.

어차피 서버의 자원을 사용해서 Refresh Token을 관리한다면, 쉽게 생성할 수 있는 고유값을 가지는 UUID로 하는 것이 장점이 있어보였다.

왜 Refresh Token의 만료기간이 짧게 남으면 만료기간을 7일로 갱신해줬나?

일단 카카오 OAuth의 Refresh Token 자동 재발급에서 영감을 받았다.

다른 서비스에서 로그인이 몇일 주기로 풀리는 현상에 불편함을 느꼈었고, 자주 접속하는 사용자라면 로그인을 계속 유지시켜주는 게 좋겠다고 생각했다.

근데 왜 카카오처럼 Refresh Token을 새걸로 갈아주는 게 아니라 현재 토큰의 만료기간을 늘려주었을까?

일단 JWT가 아니고 UUID이기 때문에 토큰의 바디가 바뀌지 않는다.

서버에서 해당 UUID값의 토큰의 유효 기간을 늘려주는 식으로 관리할 수 있다.

이걸 이용해서 기간이 짧게 남은 Refresh Token을 남겨두고 새 토큰을 찍어내는 방법보다 더 라이트하게 로그인을 연장시켜줄 수 있었다.

그럼 Refresh Token이 무한으로 연장될 수도 있는데, 탈취시 취약점은 없나?

일단, 새로운 Refresh Token으로 로그인을 연장시키는 방법도 같은 취약점을 갖는다. 오로지 Refresh Token으로 새 Refresh Token을 받을 수 있게끔 하면 어차피 탈취한 쪽에서 매번 새걸로 갈아서 쓰면 그만이다.

같은 취약점을 가진다고 생각했다. 이를 해소하기 위해 서버에서 관리하는 Refresh Token을 무효화시킬 수 있도록 구현했다. 사용자가 신고를 눌러서 무효화 시킬 수 있고, 서버에서 의심이 된다면 무효화시키는 방안도 고민중이다.

왜 리프레시 토큰을 계정당 3개까지만 관리하는가?

서버의 자원은 무한대가 아니다. 로그인마다 새 리프레시 토큰을 생성하고 관리하면 한 유저가 여러 로그인을 반복할 때마다 리프레시 토큰이 생성된다.
그래서 구글 OAuth에서 영감을 받아 리프레시 토큰 최대 갯수를 제한하기로 했다.

3개로 정한 이유는 내편 서비스는 pc, 모바일 정도로만 접속할 가능성이 높기 때문에 3개면 여유롭게 사용한다고 생각했다.
한 디바이스에서 여러 디바이스로 접속하는 경우는 고려하지 않았다. 이유는 사실 특정 디바이스마다 자주 사용하는 브라우저가 있기 때문에 해당 브라우저로만 접속할 것으로 생각하고,
여유분 1개를 더 둬서 3개까지 관리하는 정책을 정했다.

 

탈취를 판단하는 로직은 어떻게..?

서버에서 탈취를 의심하게 하는 방법도 고민해봤는데, 리프레시 토큰을 처음 접속한 기기 ID와 함께 저장하여 처음 접속하지 않은 기기에서 요청이 들어올 경우 해당 리프레시 토큰을 무효화시키는 방법이 떠올랐었다. 단점은 기기정보를 얻어내는 외부 라이브러리를 요청 보내는 웹에서 사용해서 넘겨줘야한다는 단점이 있었는데, 내편 서비스에선 이 로직에 대해선 좀 더 고려중이다.