리프레시 토큰
고친 사람 github-actions[bot]
리프레시 토큰은 사람을 다시 로그인시키지 않고 새 출입증을 받아 옵니다. 남의 서비스를 부를 때 내미는 출입증에는 짧은 기한이 붙습니다. 기한이 끝나면 앱이 이 문자열을 내밀고 새 출입증을 받아 하던 일을 이어갑니다.
쉽고 빠른 이해
리프레시 토큰은 그 출입증(액세스 토큰)을 새로 받아 오는 데만 쓰는 문자열입니다. 메일 앱이 새벽에 메일을 가져올 때, 자고 있는 사용자를 깨워 로그인시키는 대신 이 문자열을 보내 새 액세스 토큰을 받습니다.
이게 없으면 둘 중 하나를 골라야 합니다. 액세스 토큰의 기한을 길게 늘리는 것이 하나, 기한이 끝날 때마다 사람을 다시 로그인시키는 것이 다른 하나입니다. 앞쪽은 토큰이 새 나가면 오래 쓰입니다. 뒤쪽은 사람이 없는 동안 일이 멈춥니다.
어떻게 도나:
- 로그인할 때 액세스 토큰과 리프레시 토큰을 함께 받습니다
- 액세스 토큰의 기한이 끝나면 리프레시 토큰을 발급해 준 서버로 보냅니다
- 새 액세스 토큰이 돌아옵니다. 하던 요청을 다시 보냅니다
대가는 오래 사는 비밀이 하나 더 생긴다는 것입니다. 훔친 사람도 새 액세스 토큰을 계속 받아 갈 수 있어서, 이 문자열을 어디에 두고 언제 폐기할지를 따로 정해야 합니다.
상세
박람회에 사전 등록하면 문자로 바코드 하나가 옵니다. 전시장 입구는 목에 건 출입증만 보고 들여보냅니다. 바코드는 등록 데스크에 내밀어 그 출입증으로 바꿔 받는 데만 씁니다.
이 절은 먼저 두 토큰이 무엇을 맡는지 가릅니다. 이어서 새 액세스 토큰을 받아 오는 한 왕복을 따라갑니다. 그다음 액세스 토큰의 기한을 왜 짧게 두는지, 리프레시 토큰이 새 나가면 무엇이 벌어지는지를 봅니다.
액세스 토큰과의 차이
액세스 토큰은 데이터를 요청할 때 내미는 출입증입니다. 리프레시 토큰은 그 출입증을 새로 받아 오는 데 쓰는 문자열입니다. 액세스 토큰은 보호된 데이터를 들고 있는 자원 서버에 내밉니다. 리프레시 토큰은 토큰을 발급한 인가 서버에만 냅니다.
둘을 가르는 까닭은 내미는 상대가 다르다는 것입니다. 자원 서버를 부르는 요청은 수없이 오가므로 그때마다 액세스 토큰이 함께 돌아다닙니다. 리프레시 토큰은 기한이 끝날 때만 한 서버로 갑니다. 훨씬 덜 돌아다니므로 새 나갈 틈도 좁습니다.
둘을 나란히 놓으면 이렇습니다. 마지막 줄의 「새 나가면」은 뒤에서 절 하나를 따로 씁니다.
| 액세스 토큰 | 리프레시 토큰 | |
|---|---|---|
| 내미는 상대 | 자원 서버 | 인가 서버 |
| 하는 일 | 데이터를 읽고 쓴다 | 새 액세스 토큰을 받는다 |
| 기한 | 짧다 | 길다 |
| 새 나가면 | 기한이 끝날 때까지 남의 데이터가 열린다 | 폐기하기 전까지 새 액세스 토큰이 계속 나온다 |
기한의 길이는 못 박힌 값이 아니라 서비스가 고르는 것입니다. 액세스 토큰은 분 단위로, 리프레시 토큰은 날이나 주 단위로 두는 것이 흔합니다.
새 액세스 토큰을 받아 오는 한 왕복
앱은 액세스 토큰이 언제 만료되는지를 미리 알거나, 만료됐다는 응답을 받고서야 압니다. 어느 쪽이든 그다음에 하는 일은 같습니다.
sequenceDiagram
participant 앱
participant 자원서버 as 자원 서버
participant 인가서버 as 인가 서버
앱->>자원서버: 액세스 토큰을 내밀고 데이터를 요청한다
자원서버-->>앱: 토큰의 기한이 끝났다
앱->>인가서버: 리프레시 토큰을 보낸다
인가서버-->>앱: 새 액세스 토큰
앱->>자원서버: 같은 요청을 다시 보낸다
자원서버-->>앱: 데이터
이 왕복에 사용자는 끼지 않습니다. 앱과 인가 서버 둘이서만 주고받으므로 화면을 띄울 일도, 비밀번호를 물을 일도 없습니다. 사용자 눈에는 앱이 그냥 계속 도는 것으로 보입니다.
새 토큰이 돌아오는 응답에 설명을 달아 보면 이런 꼴입니다. 오른쪽 주석은 읽기 쉬우라고 붙인 것이고 서버가 보내는 응답에는 없습니다.
{
"access_token": "2YotnFZ…", // 새 출입증
"expires_in": 3600, // 남은 초
"refresh_token": "tGzv3JO…" // 다음에 쓸 것
}
예시의 마지막 줄처럼 새 리프레시 토큰이 함께 오기도 합니다. 그때는 옛것을 폐기하고 새것을 저장해 두어야 다음 갱신이 됩니다. 옛것을 계속 쓰면 그다음 갱신에서 거절당합니다.
새로 받는 액세스 토큰이 할 수 있는 일은 처음 것과 같거나 더 좁습니다. 무엇을 해도 되는지를 가리키는 스코프를 갱신하면서 넓힐 수는 없습니다. 넓히려면 사용자에게 다시 물어야 합니다.
액세스 토큰의 기한을 짧게 두는 까닭
기한을 짧게 두는 까닭은 자원 서버가 토큰을 받을 때마다 인가 서버에 물어보지 않기 때문입니다.
자원 서버는 대개 토큰에 붙은 서명을 확인하거나 미리 받아 둔 정보로 판정합니다. 물어보지 않는 까닭은 둘입니다. 요청마다 서버를 한 번 더 거치면 응답이 늦어집니다. 인가 서버가 멈추면 모든 요청이 함께 멈춥니다.
물어보지 않는 대신 기한으로 끊습니다. 사용자가 권한을 거둬들여도 이미 나간 액세스 토큰은 기한이 끝날 때까지 통합니다. 기한이 짧을수록 그 틈이 줄어듭니다.
그런데 기한을 줄일수록 사람이 다시 로그인해야 하는 횟수가 늘어납니다. 리프레시 토큰은 이 둘을 갈라 놓습니다. 사람에게 묻는 일은 처음 한 번으로 끝냅니다. 짧은 기한은 앱이 알아서 갱신합니다.
새 나갔을 때의 피해와 대응
리프레시 토큰은 가진 사람이면 누구든 쓸 수 있습니다. 훔친 쪽도 새 액세스 토큰을 받아 갑니다. 토큰은 문자열이라 복사해 가도 원래 쪽에 그대로 남습니다. 훔친 쪽과 원래 쪽이 같은 토큰을 동시에 씁니다. 그 리프레시 토큰이 살아 있는 동안 그 일이 되풀이됩니다.
막는 장치는 셋입니다. 토큰을 어디에 두는가, 어떻게 폐기하는가, 새 나간 것을 어떻게 알아채는가입니다.
먼저 어디에 두는가입니다. 앱은 두 갈래입니다. 하나는 앱의 서버에서 돕니다. 다른 하나는 사용자 브라우저나 기기에서 돕니다.
앱의 서버가 토큰을 들고 있으면 사용자 브라우저를 지나지 않아 새 나갈 길이 하나 줄어듭니다. 브라우저 안의 스크립트가 읽을 수 있는 데 두면 스크립트 한 줄이 새는 순간 토큰도 함께 샙니다.
다음은 폐기입니다. 인가 서버는 발급한 리프레시 토큰을 언제든 폐기할 수 있어야 합니다. 폐기를 무효화라고도 부릅니다. 이 글에서는 폐기로 적습니다.
사용자가 앱과의 연결을 끊거나 비밀번호를 바꾸면 그 사용자의 리프레시 토큰을 전부 폐기합니다.
마지막은 회전입니다. 갱신할 때마다 리프레시 토큰도 새것으로 바꿔 줍니다. 앱이 낸 옛 토큰은 바로 폐기합니다.
폐기한 토큰이 다시 오면 누군가가 복사본을 들고 있다는 신호입니다. 그때는 그 사용자의 토큰을 모두 폐기하고 다시 로그인시킵니다.
flowchart TD
A["앱이 리프레시 토큰을 낸다"] --> B{"처음 쓰는 토큰인가"}
B -->|처음 쓴다| C["새 액세스 토큰과 새 리프레시 토큰을 주고 낸 토큰은 바로 폐기한다"]
B -->|폐기한 토큰이 다시 왔다| D["복사본 신호. 그 사용자의 토큰을 전부 폐기한다"]
쓰는 경우와 안 쓰는 경우
사람이 앞에 없는 동안에도 앱이 남의 서비스를 계속 불러야 할 때 씁니다. 배경에서 메일을 받아 오는 앱, 밤사이 사진을 올리는 작업이 그런 경우입니다.
사용자가 화면을 보고 있는 짧은 방문에는 없어도 됩니다. 앱이 자기 사용자를 직접 로그인시키는 경우라면 세션을 쿠키에 실어 주는 쪽이 부품이 적습니다. 갱신도 폐기도 서버 안에서 끝납니다.
서버끼리 부르는 통신에도 쓰지 않습니다. 이때는 부르는 쪽이 앱마다 발급된 아이디와 비밀값으로 언제든 새 액세스 토큰을 받습니다. 이 아이디와 비밀값은 사용자의 것이 아니라 앱 자신의 것입니다. 따로 기억해 둘 토큰이 필요 없습니다.
그런데 그 비밀값을 숨길 데가 없는 앱도 있습니다. 브라우저에서 도는 화면과 사용자 기기에 설치된 앱이 여기 듭니다. 코드가 사용자 손에 있어서 그 안에 적어 둔 비밀값은 누구든 들여다볼 수 있습니다.
이런 앱에는 리프레시 토큰을 그냥 내주지 않습니다. 장치는 둘입니다.
하나는 앞에서 본 회전입니다. 한 번 쓰면 그 토큰을 폐기하고 새것으로 바꿔 주므로 훔친 복사본은 오래 못 쓰입니다. 다른 하나는 그 앱만 가진 열쇠로 서명해야 갱신이 통하도록 묶는 것입니다. 토큰만 훔쳐서는 갱신이 안 됩니다.
관련 항목
한 번의 로그인에서 같이 발급되는 토큰
액세스 토큰 · ID 토큰 · 인가 코드 · Bearer 토큰 · 불투명 토큰 · JWT · 토큰
리프레시 토큰을 발급하고 폐기하는 주체
인가 서버 · 토큰 엔드포인트 · 클라이언트 · 자원 서버 · 자원 소유자 · 인증 서버
리프레시 토큰을 정의하는 표준·프로토콜
OAuth 2.0 · OpenID Connect · 토큰 폐기 · 토큰 인트로스펙션 · PKCE · SAML
리프레시 토큰이 실어 나르는 권한
스코프 · 권한 · 인가 · 인증 · 인증과 인가 · 최소 권한 원칙
리프레시 토큰을 담아 두는 저장 수단
쿠키 · 세션 · 로컬 스토리지 · 키체인 · 시크릿 관리 · HTTPS
리프레시 토큰이 새었을 때 벌어지는 공격
토큰 탈취 · 재전송 공격 · 크로스 사이트 스크립팅 · 크로스 사이트 요청 위조 · 중간자 공격 · 세션 고정
리프레시 토큰의 수명을 다루는 장치
토큰 회전 · 토큰 만료 · 무효화 · 재인증 · 슬라이딩 만료 · 싱글 사인온
리프레시 토큰을 대신하는 로그인 유지 방식
다른 이름: refresh token