diff --git a/src/main/java/com/Timo/Timo/global/auth/factory/AuthResponseFactory.java b/src/main/java/com/Timo/Timo/global/auth/factory/AuthResponseFactory.java index c470653b..dc5b9ab5 100644 --- a/src/main/java/com/Timo/Timo/global/auth/factory/AuthResponseFactory.java +++ b/src/main/java/com/Timo/Timo/global/auth/factory/AuthResponseFactory.java @@ -33,11 +33,21 @@ public ResponseEntity> reissueResponse(Reissue .accessToken(result.getAccessToken()) .build(); - return ResponseEntity.ok() + ResponseEntity.BodyBuilder builder = ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, refreshTokenCookie(result.getRefreshToken())) .header(HttpHeaders.SET_COOKIE, sessionIdCookie(result.getSessionId())) - .header("Cache-Control", "no-store") - .body(BaseResponse.onSuccess(AuthSuccessCode.REISSUE_SUCCESS, body)); + .header("Cache-Control", "no-store"); + + addLegacyCookieCleanup(builder); + + return builder.body(BaseResponse.onSuccess(AuthSuccessCode.REISSUE_SUCCESS, body)); + } + + private void addLegacyCookieCleanup(ResponseEntity.BodyBuilder builder) { + if (cookieSecure) { + builder.header(HttpHeaders.SET_COOKIE, CookieUtil.expireLegacyCookie("refreshToken").toString()); + builder.header(HttpHeaders.SET_COOKIE, CookieUtil.expireLegacyCookie("sessionId").toString()); + } } public ResponseEntity> logoutResponse() { @@ -49,11 +59,14 @@ public ResponseEntity> withdrawResponse() { } private ResponseEntity> expiredCookieResponse(AuthSuccessCode successCode) { - return ResponseEntity.ok() + ResponseEntity.BodyBuilder builder = ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, CookieUtil.expireCookie("refreshToken", cookieSecure).toString()) .header(HttpHeaders.SET_COOKIE, CookieUtil.expireCookie("sessionId", cookieSecure).toString()) - .header(HttpHeaders.CACHE_CONTROL, "no-store") - .body(BaseResponse.onSuccess(successCode, null)); + .header(HttpHeaders.CACHE_CONTROL, "no-store"); + + addLegacyCookieCleanup(builder); + + return builder.body(BaseResponse.onSuccess(successCode, null)); } private String refreshTokenCookie(String refreshToken) { diff --git a/src/main/java/com/Timo/Timo/global/auth/handler/OAuthSuccessHandler.java b/src/main/java/com/Timo/Timo/global/auth/handler/OAuthSuccessHandler.java index f8146c3a..88d42076 100644 --- a/src/main/java/com/Timo/Timo/global/auth/handler/OAuthSuccessHandler.java +++ b/src/main/java/com/Timo/Timo/global/auth/handler/OAuthSuccessHandler.java @@ -58,6 +58,11 @@ public void onAuthenticationSuccess( CookieUtil.createCookie("sessionId", sessionId, jwtTokenProvider.getRefreshTokenExpiry(), cookieSecure).toString()); + if (cookieSecure) { + response.addHeader(HttpHeaders.SET_COOKIE, CookieUtil.expireLegacyCookie("refreshToken").toString()); + response.addHeader(HttpHeaders.SET_COOKIE, CookieUtil.expireLegacyCookie("sessionId").toString()); + } + String code = authCodeService.generateAndSave( String.valueOf(userId), onboardingCompleted diff --git a/src/main/java/com/Timo/Timo/global/auth/service/AuthService.java b/src/main/java/com/Timo/Timo/global/auth/service/AuthService.java index 6f1db3cb..26bfe47d 100644 --- a/src/main/java/com/Timo/Timo/global/auth/service/AuthService.java +++ b/src/main/java/com/Timo/Timo/global/auth/service/AuthService.java @@ -1,6 +1,5 @@ package com.Timo.Timo.global.auth.service; -import com.Timo.Timo.domain.calendar.client.GoogleOAuthClient; import com.Timo.Timo.domain.calendar.entity.CalendarRevocationOutbox; import com.Timo.Timo.domain.calendar.repository.CalendarConnectionRepository; import com.Timo.Timo.domain.calendar.repository.CalendarRevocationOutboxRepository; @@ -76,22 +75,33 @@ public ReissueResult reissue(String refreshToken, String sessionId) { } Long userId = jwtTokenProvider.getUserId(refreshToken); + String userIdKey = String.valueOf(userId); if (!userRepository.existsById(userId)) { throw new CustomException(UserErrorCode.USER_NOT_FOUND); } - if (!refreshTokenService.isRefreshTokenValid(String.valueOf(userId), sessionId, refreshToken)){ - throw new CustomException(AuthErrorCode.INVALID_REFRESH_TOKEN); + if (refreshTokenService.isRefreshTokenValid(userIdKey, sessionId, refreshToken)) { + String newAccessToken = jwtTokenProvider.generateAccessToken(userId); + String newRefreshToken = jwtTokenProvider.generateRefreshToken(userId); + String newSessionId = refreshTokenService.rotateRefreshToken(userIdKey, sessionId, newRefreshToken); + + return new ReissueResult(newAccessToken, newRefreshToken, newSessionId); } - refreshTokenService.deleteRefreshToken(String.valueOf(userId), sessionId); + return refreshTokenService.findRotatedSessionId(userIdKey, sessionId) + .map(newSessionId -> reissueFromAlreadyRotatedSession(userId, userIdKey, newSessionId)) + .orElseThrow(() -> new CustomException(AuthErrorCode.INVALID_REFRESH_TOKEN)); + } - String newAccessToken = jwtTokenProvider.generateAccessToken(userId); - String newRefreshToken = jwtTokenProvider.generateRefreshToken(userId); - String newSessionId = refreshTokenService.saveRefreshToken(String.valueOf(userId), newRefreshToken); + private ReissueResult reissueFromAlreadyRotatedSession(Long userId, String userIdKey, String newSessionId) { + String currentRefreshToken = refreshTokenService.getRefreshToken(userIdKey, newSessionId); + if (currentRefreshToken == null) { + throw new CustomException(AuthErrorCode.INVALID_REFRESH_TOKEN); + } - return new ReissueResult(newAccessToken, newRefreshToken, newSessionId); + String newAccessToken = jwtTokenProvider.generateAccessToken(userId); + return new ReissueResult(newAccessToken, currentRefreshToken, newSessionId); } public void logout(String accessToken, Long userId, String sessionId) { diff --git a/src/main/java/com/Timo/Timo/global/auth/service/RefreshTokenService.java b/src/main/java/com/Timo/Timo/global/auth/service/RefreshTokenService.java index ef704d23..b0a64f6b 100644 --- a/src/main/java/com/Timo/Timo/global/auth/service/RefreshTokenService.java +++ b/src/main/java/com/Timo/Timo/global/auth/service/RefreshTokenService.java @@ -4,7 +4,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Set; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; @@ -21,6 +21,8 @@ public class RefreshTokenService { private final JwtTokenProvider jwtTokenProvider; private static final String KEY_PREFIX = "refresh:"; + private static final String ROTATED_PREFIX = "refresh:rotated:"; + private static final long ROTATION_GRACE_SECONDS = 5; public String saveRefreshToken(String userId, String refreshToken){ String sessionId = UUID.randomUUID().toString(); @@ -63,4 +65,24 @@ public void deleteAllRefreshTokens(String userId) { public boolean isRefreshTokenValid(String userId, String sessionId, String refreshToken) { return Objects.equals(refreshToken, getRefreshToken(userId, sessionId)); } + + public String rotateRefreshToken(String userId, String oldSessionId, String newRefreshToken) { + String newSessionId = saveRefreshToken(userId, newRefreshToken); + + redisTemplate.opsForValue().set( + ROTATED_PREFIX + userId + ":" + oldSessionId, + newSessionId, + ROTATION_GRACE_SECONDS, + TimeUnit.SECONDS + ); + + deleteRefreshToken(userId, oldSessionId); + return newSessionId; + } + + public Optional findRotatedSessionId(String userId, String oldSessionId) { + return Optional.ofNullable( + redisTemplate.opsForValue().get(ROTATED_PREFIX + userId + ":" + oldSessionId) + ); + } } diff --git a/src/main/java/com/Timo/Timo/global/auth/utils/CookieUtil.java b/src/main/java/com/Timo/Timo/global/auth/utils/CookieUtil.java index 8d2a2382..e3949ae5 100644 --- a/src/main/java/com/Timo/Timo/global/auth/utils/CookieUtil.java +++ b/src/main/java/com/Timo/Timo/global/auth/utils/CookieUtil.java @@ -34,4 +34,14 @@ public static ResponseCookie expireCookie(String name, boolean secure) { return builder.build(); } + + public static ResponseCookie expireLegacyCookie(String name) { + return ResponseCookie.from(name, "") + .httpOnly(true) + .secure(true) + .path("/api/v1/auth") + .maxAge(0) + .sameSite("None") + .build(); + } }