TrendVidStream은 탈취된 토큰이 재사용되는 즉시 이를 감지하는 회전식 리프레시 토큰(rotating-refresh-token) 시스템으로 전체 인증 스택을 전환했습니다. 이 변화를 통해, 한때 공격자가 자유롭게 활동할 수 있게 했던 30일 유효기간의 JWT는 사용 즉시 계정 잠금을 유발하는 수명이 짧은 인증 정보로 바뀌었습니다.
단 한 번의 보안 침해 사고가 이번 전환을 강제했습니다. 파트너 SDK가 30일 유효기간의 JWT를 평문으로 캐싱했고, 공격자가 이를 추출하여 다른 국가에서 재전송(replay)했습니다. 당시 유일한 해결책은 서명 키를 교체(rotate)하는 것이었는데, 이 작업으로 인해 모든 사용자가 로그아웃되어야 했습니다. 이 사건은 회사의 토큰 보안 체계를 재편했으며, 현재는 비디오 스트리밍 서비스의 핵심 동력으로 작동하고 있습니다.
기존 모델이 실패한 이유
JWT(JSON Web Tokens)는 서버가 데이터베이스 조회 없이도 요청을 검증할 수 있게 해주는, 자체 포함된 서명된 블롭(blobs)입니다. 이러한 편리함 뒤에는 대가가 따릅니다. 토큰의 유효기간이 몇 주라면, 이를 탈취한 공격자는 몇 주 동안 접근 권한을 갖게 됩니다. 이번 침해 사고에서 탈취된 토큰은 조기에 무효화할 방법이 없었기 때문에 30일의 만료 기간이 지날 때까지 유효한 상태로 남아 있었습니다.
서명 키를 교체하는 것은 모든 토큰을 무효화할 수 있는 유일한 전역적 방법이지만, 이는 모든 사용자가 다시 로그인해야 함을 의미하며, 서비스 중단과 신뢰 저하를 초래합니다. 결함은 JWT 자체에 있었던 것이 아니라, 단일한 장기 유효 인증 정보에 의존했다는 점에 있었습니다.
새로운 설계의 핵심 요약
TrendVidStream은 이제 두 가지 종류의 토큰을 발급합니다.
- Access tokens – 15분 동안 유효하며, 각 API 호출에 필요한 권한을 담고 있습니다.
- Refresh tokens – 수명이 짧은 액세스 토큰을 새로운 한 쌍의 토큰으로 교체하는 데 사용되는 일회용 토큰입니다.
클라이언트가 리프레시 토큰을 제시하면 서버는 다음 과정을 수행합니다.
- 토큰의 서명과 클레임(claims)을 검증합니다.
- 해당 토큰이 이미 사용되었는지 확인합니다.
- 확인 결과 문제가 없다면, 완전히 새로운 리프레시 토큰과 새로운 15분 유효 액세스 토큰을 발급합니다.
- 기존 리프레시 토큰을 '사용됨' 상태로 표시합니다.
만약 2단계에서 실패한다면(즉, 동일한 토큰이 두 번째로 나타난다면), 서버는 이를 탈취 신호로 간주하고 해당 로그인 세션과 연결된 토큰 '패밀리(family)' 전체를 무효화합니다. 피해자와 공격자 모두 다시 로그인해야 하므로 침해 사고를 조기에 차단할 수 있습니다.
토큰 패밀리 추적하기
시스템은 각 토큰을 개별 레코드로 취급하는 대신, 사용자가 로그인할 때 시작되는 '패밀리' 단위로 그룹화합니다. 모든 로테이션은 해당 패밀리의 새로운 구성원을 생성합니다. 실제 운영 환경에서 사용되는 SQLite 스키마는 다음을 저장합니다.
- family_id – 전체 세션을 식별하는 고정된 식별자.
- token_id – 각 리프레시 토큰의 고유 식별자.
- generation – 디버깅에 유용한 카운터.
- used_at – 토큰이 사용된 시점의 타임스탬프.
- revoked – 설정 시 해당 패밀리 전체를 비활성화하는 플래그.
재사용 이벤트가 감지되면 해당 family_id의 revoked 플래그가 설정되어, 침해된 세션에 속한 모든 토큰이 즉시 무효화됩니다. 이를 통해 조용한 하이재킹을 로그에 명확히 나타나는 높은 신호의 경고로 전환할 수 있습니다.
시스템의 신뢰성을 유지하는 세 가지 구현 규칙
서명을 가장 먼저 검증하라 만약 서버가 서명을 검증하기 전에 "이미 사용되었는가?"를 먼저 확인한다면, 공격자가 토큰 ID를 추측하여 대규모 무효화 공격을 유도할 수 있습니다. 인증을 먼저 확인하면 불필요한 서비스 거부(DoS) 공격을 방지할 수 있습니다.
유예 기간(grace window)을 허용하라 모바일 앱은 토큰이 만료되었을 때 종종 두 개의 리프레시 요청을 연속해서 보냅니다. 서버가 너무 엄격하면 두 번째 요청이 재사용으로 간주되어 정상적인 사용자가 로그아웃될 수 있습니다. 몇 초간의 버퍼를 두면 '사용된' 토큰이라도 새로운 토큰 쌍을 반환할 수 있어 레이스 컨디션(race condition) 문제를 완화할 수 있습니다.
전체 흐름을 행 잠금(row locking)이 포함된 트랜잭션으로 감싸라 원자성(atomicity)이 보장되지 않으면, 두 개의 동시 요청이 모두 자신이 토큰을 처음 사용하는 것이라고 판단하여 중복된 리프레시 토큰을 발급하고 일회용 보장 원칙을 깨뜨릴 수 있습니다. 토큰 행을 잠그는 데이터베이스 트랜잭션을 사용하면 단 하나의 요청만 성공하도록 보장할 수 있습니다.
핵심 요약: 리프레시 토큰을 일회용으로 만들고 재사용 여부를 감시함으로써, 시스템은 모든 탈취된 인증 정보를 경보로 전환하여 플랫폼 전체의 로그아웃 없이도 사용자를 보호할 수 있습니다.
