본문 바로가기
Back-end/SpringBoot

[Security] refresh token rotation 방식을 쓰는 이유

by backend 개발자 지망생 2025. 6. 3.

PREV. 서론

refresh token rotation 방식을 도입하게 되면서 첫 번째 구조로서 refresh token을 redis에 access token을 http-only cookie에 두어서 해결하려 했다. 하지만 만료된 access token으로도 user를 식별할 수 있는 로직 설계에서 어렵다는 생각을 하였고, 또한 세션 기반 인증 방식에 가깝다는 생각이 들었다.

초기에 생각

그러한 생각의 대안으로 OIDC,OAuth 2.1의 흐름에 따라, Refresh Token을 쿠키로 내리는 정책으로 진행했다. 토큰 자체에 상태가 담겨있고, Redis는 단지 대조,회전만을 목적으로 하여 jwt 방식과 비슷하다고 생각하였다.

 

1. Refresh Token Rotation이란 무엇인가?

  1. 정의
    • Refresh Token Rotation(이하 “회전 방식”)은 클라이언트가 리프레시 토큰을 이용해 새로운 액세스 토큰을 발급받을 때마다, 기존에 사용한 리프레시 토큰을 폐기하고 새로운 리프레시 토큰을 함께 발급받는 방식.
    • 즉, 리프레시 토큰을 한번 쓰면 그 토큰은 더 이상 유효하지 않고, 서버는 새로운 리프레시 토큰을 클라이언트에게 제공하는 방식.
  2. 동작 흐름
    1. 사용자가 최초 인증을 통해 발급받은 액세스 토큰과 리프레시 토큰이 존재.
    2. 액세스 토큰이 만료되었을 때, 클라이언트는 저장해 둔 리프레시 토큰을 Authorization 서버에 제출하여 “새로운” 액세스 & 리프레시 토큰을 요청한다.
    3. 서버는 제출된 리프레시 토큰을 즉시 무효화하고, 그 대신 새로운 리프레시 토큰을 발급해 반환한다.
    4. 클라이언트는 향후 액세스 토큰 재발급 시점에 새롭게 받은 리프레시 토큰만 사용하며, 한번 사용한 토큰은 재사용하지 않는다.

2. RFC(Reqeust For comments) 6819에서 권고한 배경

  1. 초기 구현체는 한 번 발급된 리프레시 토큰을 만료 시점까지 그대로 사용(Static)하는 구조를 택함
  2. 정적 리프레시 토큰의 경우, 만약 공격자가 사용자의 액세스 토큰을 탈취(CSRF, 세션 하이재킹 등)했다면 refresh 엔드포인트를 호출 시에 “유효한 액세스 토큰”을 제출해버리면, 서버는 해당 쿠키와 매핑된 redis(서버) 상의 리프레시 토큰을 사용해 새로운 액세스 토큰을 발급.
    1. 결국 액세스 토큰이 노출되면 해당 세션 전체가 위험(리프레시 토큰 세션).
  3. 이러한 점을 리프레시 토큰 회전을 제시함으로써, 정적 토큰 방식의 취약점(토큰 탈취 시 무제한 재사용)을 완화할 수 있다고 함

3. 표준이 된 이유

  1. 공격 표면 최소화
    • 리프레시 토큰을 한번 쓰면 무효화하기 때문에, 탈취 시점 이후에는 탈취된 토큰으로 더 이상 새로운 액세스 토큰을 발급받을 수 없다.
    • 이를 통해 토큰 탈취 사고 발생 시 피해 범위를 “리프레시 토큰이 사용되기 전까지”로 한정할 수 있다.
  2. 안전한 재발급
    • 서버 측은 이미 사용한 리프레시 토큰이 다시 제출될 경우 이를 탐지하여 공격 징후로 판단하고 추가 조치를 취할 수 있다. (ex. 토큰을 즉시 모두 폐기 or 사용자의 세션을 강제 종료)

Static refresh token vs Rotate refresh token

구분 정적(Static) 리프레시 토큰 회전(Rotation) 리프레시 토큰

토큰 사용 주기 클라이언트가 리프레시 토큰을 받아 한번도 바뀌지 않고 만료 시까지(또는 서버가 별도 폐기 시까지) 계속 사용 클라이언트가 매번 리프레시 허가 요청 시마다 새로운 리프레시 토큰 발급 → 이전 토큰 즉시 폐기
토큰 탈취 시점·영향 범위 탈취 시 만료 전까지 계속 사용 가능 (공격자는 만료 시까지 무제한 재발급) 탈취된 토큰이 이미 한 번 사용된 상태라면 회수되어 효력 없음. 사용 전 탈취당했다면 한 번만 재발급 가능
재사용 탐지(Replay Detection) 별도 로직이 없으면 탐지 불가능. 탈취 사실을 알 수 있는 지표가 적음 무효화된 토큰 제출 시 탐지 가능 → 공격 징후로 판단해 추가 조치(세션 종료 등)
보안 관리 부담 토큰 탈취 시 즉각 알기 어려움. 장기간 유효 기간 설정 시 매우 위험 토큰을 짧은 생명주기로 회전시키므로 탈취 피해를 빠르게 차단할 수 있음.
서버 측 구현 복잡도 간단: DB나 메모리에 토큰을 저장하고, 만료/폐기 정책만 관리 매번 “발급 → 폐기” 로직을 구현해야 하므로, 토큰 저장소에서 상태 관리(사용 여부, 회수 여부)tok 필요

Access token & Refresh token의 filter 도달 시 경우의 수

prev. public api & 회원 탈퇴 & 여러 개의 요청이 queue에 있을 때의 경우는 제외

 

JwtAuthenticationFilter에 도달했을 때의 경우의 수에서 flow를 추려봤다.

 

1.1 access token이 살아있을 때 & 유효할 때

  1.1.1 token != null

  1.1.2 tokenSerivce.authenticationAccess()에서 서명 검증

  1.1.3 securityContextHolder에 principal 세팅

1.2 access token이 살아있을 때 & 서명 오류

  1.2.1 tokenSerivce.authenticationAccess()에서 서명 검증

  1.2.2 JwtExcpetion 분기로 넘어가서 401(access token invalid) 반환

  1.2.3 강제 로그아웃

1.3 access token이 만료되어서 사라졌을 때(refresh token은 살아있음)

  1.3.1 token == null → doFilter(...)로 넘김

  1.3.2 보호된 api(.authenticated) 호출 시 “인증 정보 없음 → 401 Unauthorized” 응답

  1.3.3 클라이언트: 401 받으면 → 자동으로 POST /api/auth/oauth2/refresh 호출

  1.3.4 서버(리프레시 컨트롤러): Refresh token 검증 → 새 토큰 발급 → 200 응답(새 쿠키)

  1.3.5 클라이언트: 다시 원래 API 호출 → 성공

1.4 access token 사라짐 & refresh token 사라짐

  1.4.1 token == null → doFilter(...)로 넘김

  1.4.2 보호된 api(.authenticated) 호출 시 “인증 정보 없음 → 401 Unauthorized” 응답

  1.4.3 클라이언트: 401 받으면 → 자동으로 POST /api/auth/oauth2/refresh 호출

  1.4.4 서버(리프레시 컨트롤러): Refresh token 검증 → 401 Missing refresh token

  1.4.5 클라이언트: 재로그인

1.5 access token 사라짐 & refresh token 살아있을 때 & 서명 오류

  1.5.1 token == null → doFilter(...)로 넘김

  1.5.2 보호된 api(.authenticated) 호출 시 “인증 정보 없음 → 401 Unauthorized” 응답

  1.5.3 클라이언트: 401 받으면 → 자동으로 POST /api/auth/oauth2/refresh 호출

  1.5.4 서버(리프레시 컨트롤러): Refresh token 검증 → 401 Invalid refresh token

  1.5.5 클라이언트: 재로그인