막은 계정의 액세스 토큰은 언제까지 통과하나
수명을 줄이고 재발급을 하나로 묶는다
액세스 토큰 수명은 계정을 막은 뒤 API가 더 열려 있어도 되는 시간입니다. 사내 서비스라면 15분 안팎으로 둡니다.
토큰을 내주는 인가 서버에서 이 값을 먼저 봅니다. Keycloak에서는 Access Token Lifespan, Auth0에서는 API 설정의 Token Expiration이 이 값입니다.
브라우저 앱은 리프레시 토큰 회전을 켭니다. 회전은 재발급할 때마다 리프레시 토큰도 새것으로 바꾸고 옛것을 무효로 하는 방식입니다. 회전을 켜면 앱이 같은 리프레시 토큰을 두 번 내지 않아야 하므로, 진행 중인 재발급의 Promise를 함께 기다려 재발급을 한 번만 보냅니다.
let refreshing = null; // 진행 중인 재발급async function api(path) { const call = () => fetch(path, { headers: { Authorization: `Bearer ${tokens.access}` } }); const res = await call(); if (res.status !== 401) return res; refreshing ??= refresh().finally(() => (refreshing = null)); try { await refreshing; } catch { return location.assign("/login"); } return call();}refresh()까지 포함한 전체 코드와 토큰을 서버에 두는 경우
refresh()는 /oauth/token에 grant_type=refresh_token으로 재발급을 받아 tokens를 새 쌍으로 바꿉니다. 실패하면 invalid_grant를 던져 위 코드가 로그인 화면으로 보냅니다.
let tokens = { access: "", refresh: "" }; // 로그인 응답으로 채운다async function refresh() { const r = await fetch("/oauth/token", { method: "POST", body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: tokens.refresh, client_id: "groupware", }), }); if (!r.ok) throw new Error("invalid_grant"); const t = await r.json(); tokens = { access: t.access_token, refresh: t.refresh_token ?? tokens.refresh };}이 코드는 토큰을 메모리에 두어 새로고침하면 사라집니다. 서버를 하나 더 둘 수 있으면 BFF가 토큰을 쥐고 브라우저에는 쿠키만 주는 편이 낫습니다.
Keycloak·Auth0·Okta를 기본값으로 쓰고 있다면
Keycloak은 액세스 토큰이 5분이고, 리프레시 토큰은 로그인 세션을 따라 30분 동안 쓰지 않거나 10시간이 지나면 끝납니다. Auth0는 API 액세스 토큰이 24시간이라 기본값 그대로면 막은 계정이 최대 하루 동안 API를 부릅니다. 리프레시 토큰은 Auth0가 30일, Okta가 7일 동안 쓰지 않으면 만료됩니다. Auth0는 같은 토큰을 잠깐 다시 받아 주는 Rotation Overlap Period를 둘 수 있고, 기본은 꺼져 있습니다.
막은 계정은 재발급에서야 끊긴다
퇴사한 민수의 노트북에 열린 그룹웨어는 로그인 때 받은 액세스 토큰으로 결재 목록을 계속 불러옵니다. API 서버가 토큰의 서명과 만료 시각(exp)만 보고, 토큰을 내준 인가 서버에 묻지 않기 때문입니다. 인가 서버와 API 서버가 한 서비스에 있어도, API 서버가 요청마다 DB에서 계정을 조회하지 않으면 마찬가지입니다.
끊기는 것은 액세스 토큰이 만료돼 앱이 리프레시 토큰으로 재발급을 받으려는 순간입니다. 재발급은 인가 서버만 받고, 인가 서버는 그때 계정과 권한을 다시 봅니다.
- 보내는 곳API 서버인가 서버만
- 서버가 보는 것서명과 exp계정·권한 기록
- 수명짧게길게
- 회수가 먹히는 때exp가 지나야다음 재발급
회전은 훔친 토큰을 재사용에서 잡는다
브라우저 앱은 자기를 증명할 비밀값을 숨길 곳이 없어서, 리프레시 토큰 하나만 있으면 누구든 재발급을 받습니다. 민수 브라우저의 악성 확장이 리프레시 토큰을 복사해 가도, 회전을 켜 두면 인가 서버가 이미 쓴 토큰이 다시 오는 순간 그 로그인에서 이어진 토큰 묶음 전체를 끊습니다.
이미 쓴 토큰이 다시 오면 인가 서버가 묶음 전체를 끊는다
같은 이유로 앱이 결재 목록과 공지를 동시에 부르다 둘 다 401을 받아 재발급을 두 번 보내면, 늦게 도착한 쪽이 재사용으로 잡혀 민수는 로그인 화면으로 갑니다. 재발급을 하나로 묶는 까닭입니다.
리프레시 토큰을 JWT로만 만들어 서버에 아무것도 남기지 않는다면
리프레시 토큰을 서명만 한 JWT로 만들고 서버에 아무것도 남기지 않으면 회전도 회수도 안 됩니다. 인가 서버가 이미 쓴 토큰을 알아볼 수 없기 때문입니다. 발급한 리프레시 토큰의 ID나 묶음 번호를 서버에 남겨야 회전이 됩니다.