개발자
류준열
refresh rotation
refresh rotation은 refresh로 new access를 발급할때마다, 서버가 refresh를 업데이트 하고 직전 refresh를 즉시 무효화하는 방식이다.
내가 있던 회사들에서는 refresh rotation을 하지 않았고, 그래서 자연스럽게 매일 오전 6시처럼 하루에 한번씩 로그아웃되었다.
refresh rotation에 대한 지식이 없던 내가 refresh rotation을 알게된건, AI 선생님과 nest로 서버를 만들면서이다.
refresh rotation을 하는 서버에서는 내가 기존 정답처럼 여기전 토큰 갱신 로직이 오답이란걸 알게 되었다.
궁금증
refresh rotation을 하려먼 DB에 refersh token을 해시로 저장한다.(유출 대비)
- 토큰을 왜 DB에 저장하지?
- 왜 refreshToken을 업데이트하지?
refresh token을 DB에 저장하는 이유

refresh token을 DB에 저장하는 이유는 재사용을 방지하고 문제가 있는 경우 무효화 하기 위함이다.
예를들어 서버에 refresh token의 상태를 저장하지 않으면 탈취된 refresh token을 개별적으로 폐기할 수 없어, 로그아웃 후에도 계속 재발급이 가능하며 최종 refresh token을 모르기 때문에 보안 조치를 취할때 추적이 어렵다.
하지만 refresh token을 DB에 저장하면 추적이 가능하여 문제가 있을 경우 해당 유저의 세션을 끊어버릴 수 있다.
refresh token을 업데이트 하는 이유
new access를 프론트에 발급해줄때 refresh token을 업데이트하고 있다.
async refresh(userId: number, rawRefreshToken: string) {
const user = await this.userService.findOne(userId);
...
const tokens = this.generateTokens(user.id, user.email);
await this.userService.updateRefreshToken(user.id, tokens.refreshToken);
return tokens;
}
async updateRefreshToken(id: number, refreshToken: string | null): Promise<void> {
const hashed = refreshToken ? await bcrypt.hash(refreshToken, 10) : null;
await this.userRepository.update(id, { refreshToken: hashed });
}
이렇게 하면 new access 를 발급할때마다 refresh token의 만료시간이 매번 초기화되고 영원히 로그아웃 되지 않나? 하는 궁금증이 들었다.
AI에게 물어보니 그게 맞다고 한다.
- sliding expiration: 갱신할때마다 만료기간도 함께 갱신하여 단골 유저는 계속 세션이 유지된다.
- absolute expiration: 이용빈도와 무관하게 정해진 기간이 지나면 refresh 끊김
sliding expiration에서는 refresh token이 만료되기까지 한번도 들어오지 않은 유저만 로그아웃된다.
다중기기를 허용하지 않는 제품에서는 이렇게 하는것이 보안, 사용성 모두 챙기는 방법일수도 있겠다.
또 다른관점에서는 다중기기를 허용하지 않기 위해 access 기간을 짧게 두고 refresh rotation을 써도 되겠다는 생각이 들었다.
프론트에서의 차이
refresh rotation을 하는 경우에는 갱신된 refresh token을 프론트에서 추적하고, refresh 중복요청을 고려해야 한다.
refresh rotation하지 않을때
refresh rotation이 없는(알지 못했던) 때는 아래와 같이 작성했었다. (참고)
const getNewAccessToken = async (refresh: string): Promise<string | void> => {
try {
const {
data: { access },
} = await userApi.userRefreshCreate({ refresh });
...
스토어에 access 저장,
헤더에 access 등록은 외부에서 함
...
return access;
} catch (e) {
에러처리
}
};
그런데 위 코드는 refresh rotation을 하는 경우 다음과 같은 이유로 불필요한 로그아웃을 하게 된다.
-
서버에서 refresh를 rotation하지만 프론트에서는 access만 트래킹하기 때문에 유저는 만료된 refresh를 갖게 된다. 결국 이 유저는 다음 access 만료시에 로그아웃 된다.
-
여러 API에서 동시에 401을 반환할때 getNewAccessToken이 여러번 호출되면 같은 refresh로 중복요청이 나가는데, rotation 환경에서는 첫 요청만 성공하고 나머지는 실패한다.
이를 해결하기 위해서는 아래와 같이 access,refresh를 모두 저장하고, refreshPromise를 트래킹해야 한다.
refresh rotation에서의 토큰갱신
- access, refresh를 둘 다 확인하여 갱신된 refresh를 놓치지 않는다.
- 동시에 여러 API가 401을 반환할때 refreshPromise를 트래킹하여 refresh 요청을 한번만 보낸다.
let refreshPromise: Promise<string | void> | null = null;
const getNewTokens = async (): Promise<string | void> => {
// 이미 refreshPromise가 있으면 refreshPromise를 반환
if (refreshPromise) return refreshPromise;
refreshPromise = (async () => {
try {
const {
data: { access,refresh },
} = await userApi.userRefreshCreate({ refresh });
스토어에 refresh, access 모두 저장 -> 갱신된 refresh 사용
헤더에 access 등록은 외부에서 함
return access;
} catch (e) {
에러처리
} finally {
refreshPromise = null;
}
})();
return refreshPromise;
};
토큰은 stateless라고 배웠는데
세션은 인증상태를 DB에 저장하지만, 토큰은 stateless를 지향하는데 왜 DB에 저장할까.. 의문이 들었는데, 완전한 stateless를 고집하면 보안통제가 약해지기 때문에, 하이브리드로 refresh만 DB에 해시로 저장한다고 한다.
검색하다가 읽은 글이 있는데 나와 같은 궁금증을 가졌던 사람이고, stateless라는 철학과 다중기기 로그인을 위해 refresh rotation을 선택하지 않았다고 한다.
refresh rotation을 사용할지, 아니면 정해진 기간으로 refresh를 관리할지는 선택의 문제다.