not-a-gardener 개발기 12) Refresh Token은 한번만 쓰도록, Redis Id에 대한 고민

메밀·2023년 9월 17일
0

not-a-gardener

목록 보기
12/13

1. Refresh token의 문제점

Refresh Token의 문제점을 생각해봤다.

1) 유효기간이 긴 RT가 탈취될 경우 보안은...?

이 경우 Refresh Token Rotation을 구현하여 Refresh Token의 일회성을 보장하면 해결될 듯 하다.

그런데 문제는 다음부터다.


2) 탈취범이 정상유저보다 먼저 Refresh Token을 재발급 받으면🥲?
3) 그래서 한 명의 사용자에 여러 RT가 생기면🥲?

이 문제들에 대한 내 나름대로의 해결책을 정리해본다.

2. Refresh Token Rotation

public Token refreshAccessToken(Refresh token) {
        String reqRefreshToken = token.getRefreshToken();
        ActiveGardener activeGardener = redisRepository.findById(token.getGardenerId())
                .orElseThrow(() -> new BadCredentialsException(ExceptionCode.NO_TOKEN_IN_REDIS.getCode()));
        RefreshToken savedRefreshToken = activeGardener.getRefreshToken();

        if (!reqRefreshToken.equals(savedRefreshToken.getToken())) {
            // redis의 refresh token과 일치하지 않음 -- B011
            redisRepository.deleteById(token.getGardenerId());
            throw new BadCredentialsException(ExceptionCode.INVALID_REFRESH_TOKEN.getCode());
        } else if (savedRefreshToken.getExpiredAt().isBefore(LocalDateTime.now())) {
            // refresh token 만료 -- B002
            redisRepository.deleteById(token.getGardenerId());
            throw new BadCredentialsException(ExceptionCode.REFRESH_TOKEN_EXPIRED.getCode());
        }

        // 새 access token 만들기
        AccessToken accessToken = tokenProvider.createAccessToken(activeGardener.getGardenerId(), activeGardener.getName());

        // Refresh Token Rotation
        // Access token 재발급 시 Refresh Token도 재발급
        RefreshToken newRefreshToken = new RefreshToken();
        activeGardener.updateRefreshToken(newRefreshToken);
        redisRepository.save(activeGardener);

        return new Token(accessToken.getToken(), newRefreshToken.getToken());
    }

구현 자체는 간단하다.

redis-cli

redis-cli를 통해 간단히 refresh token이 업데이트되는 걸 확인할 수 있다.

3. Redis ID를 Gardener PK로

2) 탈취범이 정상유저보다 먼저 Refresh Token을 재발급 받으면🥲?
3) 그래서 한 명의 사용자에 여러 RT가 생기면🥲?

위의 문제는 Redis ID로 유저PK를 쓰고 있어서 (나름) 손쉽게 해결되었다.

구글링을 통해 본 코드는 대체로 RT를 key로 사용해 사용자 정보를 value로 저장해 그 정보를 기반으로 AT를 재발급했다. 원래는 redis를 도입한 만큼 UserDetailsService.loadByUsername()에서 빠르게 유저 정보를 가져오려고 바꿔서 설계했던건데, Redis Id를 유저 pk로 하는데에는 좀 더 많은 이점이 있었다.

Redis Id를 유저 pk로 잡으면, Redis 서버에서 사용자와 Refresh Token을 1:1로 매치하는 것을 강제할 수 있기 때문이다.

토큰 도둑이 RT, AT 탈취 / 정상 유저보다 먼저 재발급한 경우

예상되는 흐름은 다음과 같다.

  1. 정상유저가 AT를 재발급받으려 한다.
  2. Gardener PK로 Redis를 조회해서 나온 RT는 토큰 도둑이 먼저 재발급한 RT다.
  3. 예외를 던지고 프론트에서 로그아웃 시킨다. Redis의 사용자 정보도 삭제한다.
  4. 정상 유저가 재로그인하면 클리어한 RT로 재생성!

Id를 RT로 잡으면 Redis엔 하나의 유저에 여러개의 RT가 저장되고, 어느 것이 정상 유저의 RT인지 알 수 없다.

고민은 이렇게 했는데 코드는 몇 줄 없다.
어이는 더 없다...

0개의 댓글