OAuth 액세스·리프레시 토큰 수명주기 운영
OAuth 토큰은 만료 시간을 정하는 것보다 회전·재사용 탐지·폐기 증거를 함께 운영해야 안전합니다. 발급부터 복구까지 상태와 점검 기준을 정리합니다.
OAuth 토큰 운영의 핵심은 access token을 오래 붙잡는 것이 아니라, 짧은 수명의 접근 토큰과 더 오래 보관할 수 있는 refresh token을 서로 다른 위험으로 취급하는 데 있습니다. 만료·폐기·회전·재사용 탐지를 상태 전이로 기록하면 사용자는 재로그인 없이 작업을 이어가고 운영자는 탈취 의심 세션만 끊을 수 있습니다.
OAuth 2.0의 표준 흐름과 토큰 발급·갱신 규칙은RFC 6749를 기준으로 확인하되, 현재 보안 권고는 공개 클라이언트의 PKCE와 refresh token 재사용 방어를 추가로 요구합니다. 따라서 구현 전에 제공자별 토큰 응답, 만료 단위, 회전 정책을 표로 고정하고 애플리케이션에는 원문 토큰 대신 해시와 상태만 남기는 방식이 안전한 출발점입니다.
먼저 고정할 토큰 상태와 책임 경계
access token은 API 요청을 허용하는 짧은 자격 증명이고 refresh token은 새 access token을 발급받는 더 강한 자격 증명입니다. 둘을 같은 테이블·같은 만료값·같은 로그 정책으로 다루면 짧게 줄여야 할 API 권한 때문에 갱신 수단까지 자주 끊기거나, 반대로 refresh token이 장기간 무방비로 남습니다. 토큰 자체의 문자열은 클라이언트 저장소와 전송 구간에서 노출될 수 있으므로 서버는 token_id, 사용자·클라이언트 식별자, 발급 시각, 절대 만료, 회전 대상, 폐기 사유를 별도 레코드로 관리합니다.
운영 상태는 active, rotated, revoked, expired, reuse_detected처럼 사건을 설명할 수 있게 나눕니다. rotated는 정상 갱신으로 이전 토큰이 더 이상 새 토큰을 발급하지 않는 상태이며, reuse_detected는 이미 회전된 토큰이 다시 제출된 경우입니다. 후자는 단순 만료와 다르므로 해당 토큰 계열(family)의 모든 refresh token을 폐기하고 새 인증을 요구해야 합니다. 로그에는 원문 토큰 대신 일방향 해시와 요청 시각·기기 식별자·결과 코드만 남겨 조사 가능성과 비밀 보호를 함께 확보합니다.

발급과 저장: 처음부터 회전 가능한 구조
발급 단계에서는 authorization code 교환 결과에서 access token의 expires_in, refresh token 존재 여부, scope를 검증하고 예상하지 못한 응답 필드는 실패로 처리합니다. 서버 저장소에는 refresh token의 해시만 저장하고 원문은 필요한 클라이언트 경계에 한 번만 전달합니다. 브라우저 애플리케이션이라면 제공자가 허용하는 경우 Authorization Code와 PKCE를 사용하고, 서버 세션을 보유한 웹앱이라면 HttpOnly·Secure·SameSite 쿠키와 CSRF 방어를 함께 검토합니다. 저장 매체가 바뀌어도 이 경계는 유지되어야 합니다.
갱신 요청은 이전 refresh token을 원자적으로 소비하고 새 토큰을 발급하는 한 트랜잭션으로 묶습니다. 같은 토큰으로 두 요청이 동시에 들어오면 하나만 성공하고 다른 요청은 reuse_detected로 분류되어야 합니다. 데이터베이스의 unique 제약과 조건부 update를 이용해 애플리케이션 락에만 의존하지 말고, 갱신 실패 시 access token을 무한 재시도하지 않도록 클라이언트도 401·invalid_grant·네트워크 오류를 구분합니다.

만료·회전·폐기 정책을 숫자가 아닌 사건으로 운영하기
만료 정책에는 유휴 만료와 절대 만료를 따로 둡니다. 유휴 만료는 최근 사용이 없을 때 세션을 정리하고 절대 만료는 장기간 사용된 자격 증명을 강제로 재인증하게 합니다. 값 자체는 공급자와 서비스 위험도에 따라 결정하되, 문서에는 어떤 시계(서버 UTC), 어떤 이벤트(발급·갱신·사용), 어떤 작업(폐기·알림)이 기준인지 적어야 합니다. 시계 오차를 고려해 만료 직전의 짧은 안전 여유를 두고, 이미 만료된 토큰은 갱신 대상이 아니라 재인증 대상으로 명확히 구분합니다.
정상 회전은 새 refresh token이 발급된 뒤 이전 토큰을 즉시 사용 불가로 만드는 방식입니다. 제공자가 회전을 지원하지 않는다면 서버 측 token family와 마지막 사용 시각을 기록해 재사용을 탐지하고, 탐지 시 가족 전체를 폐기합니다. 로그 보존 기간은 보안 조사에 필요한 최소 기간으로 정하고 접근 권한을 제한합니다. 운영자는 사용자에게 구체적인 비밀값을 보여주지 않고 ‘세션이 만료되어 다시 인증이 필요하다’는 결과와 지원 절차만 안내합니다.
예시: 한 번의 갱신을 증거로 남기기
예시의 입력은 사용자 U-42가 모바일 앱에서 scope=orders.read로 발급받은 access token과 refresh token R1을 갖고 있는 상황입니다. 결정은 access token의 expires_at이 30초 이내이므로 API 호출 전에 갱신하되, 서버가 R1 해시의 active 상태를 조건부로 소비하는 것입니다. 중간 산출물은 R1→R2 회전 레코드, R1의 rotated_at, 새 access token의 expires_at, 요청 결과 코드 200이 기록된 감사 이벤트입니다. 예상 결과는 다음 주문 조회가 R2로 성공하고, 같은 R1을 다시 제출하면 family 전체가 revoked로 바뀌어 401과 재인증 안내를 반환하는 것입니다.
이 예시에서 확인할 것은 토큰 문자열이 로그·오류 메시지·분석 이벤트에 들어가지 않았는지, 두 번의 동시 갱신 중 하나만 성공했는지, 갱신 지연이 발생해도 만료된 access token을 계속 재사용하지 않았는지입니다. 운영 대시보드에는 사용자별 원문 대신 토큰 family 식별자와 결과 분류만 표시하고, 재사용 탐지 이벤트는 일반 만료와 분리해 보안 담당자에게 전달합니다.
실패 경로와 복구: 무한 재시도를 끊는 기준
실패 경로는 invalid_grant를 네트워크 오류처럼 재시도하는 경우입니다. 이미 폐기된 refresh token을 계속 보내면 계정 잠금이나 탐지 신호를 오염시킬 수 있습니다. 클라이언트는 invalid_grant·reuse_detected를 받으면 로컬 토큰을 삭제하고 재인증 화면으로 이동하며, 네트워크 오류에는 제한된 지수 백오프만 적용합니다. 서버는 같은 family에서 반복 실패가 발생하면 추가 발급을 멈추고 사건 ID를 반환해 지원팀이 원인을 추적할 수 있게 합니다.
반대로 사용자가 여러 기기에서 정상적으로 로그인한 서비스라면 단일 refresh token 폐기가 모든 기기를 끊지 않도록 family를 기기·클라이언트 단위로 분리합니다. 그러나 중앙 계정 탈취가 의심되거나 사용자가 ‘모든 기기에서 로그아웃’을 요청한 경우에는 상위 세션 버전을 올려 모든 family를 무효화하는 별도 작업이 필요합니다. 이 경계가 문서와 테스트에 없으면 복구가 과도하게 넓거나 좁아집니다.
완료 조건과 점검 기록
토큰 수명 관리가 끝났다고 말하려면 코드가 동작한다는 것보다 증거가 있어야 합니다. 발급·정상 회전·동시 갱신·만료·재사용·전체 로그아웃 각각에 대해 요청 ID와 결과 분류가 남고, 원문 비밀값 검색에서 0건이며, 폐기된 토큰이 보호 API를 통과하지 않는지 확인합니다. 또한 제공자 문서의 변경 알림을 구독하고 expires_in·회전 정책이 바뀌었을 때 검토 티켓을 만들 담당자를 지정합니다.
토큰 레코드에 해시·family·절대 만료·폐기 사유가 있고 원문은 저장하지 않는다.
조건부 갱신으로 동시 요청에서 한 번만 회전되며 재사용은 별도 사건으로 기록된다.
invalid_grant, 네트워크 오류, 전체 로그아웃의 클라이언트 경로가 서로 다르다.
테스트 로그와 운영 로그에서 원문 토큰 검색 결과가 0건이다.
위 항목을 테스트 결과, 로그 샘플, 정책 버전과 함께 남기면 토큰 운영은 ‘대체로 안전하다’가 아니라 재현 가능한 완료 상태가 됩니다. 완료 조건은 원문 토큰이 로그에 없고 회전·재사용·폐기 테스트가 모두 통과했음을 확인하고 기록하는 것입니다. 제공자별 예외가 이 기준과 충돌할 때는 편의보다 범위를 좁힌 권한과 명시적 재인증을 우선하고, 예외 사유와 만료일을 기록해 다음 검토 시 제거 여부를 다시 판단합니다.