Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -81,12 +80,10 @@ public ReissueResult reissue(String refreshToken, String sessionId) {
throw new CustomException(UserErrorCode.USER_NOT_FOUND);
}

if (!refreshTokenService.isRefreshTokenValid(String.valueOf(userId), sessionId, refreshToken)){
if (!refreshTokenService.validateAndConsumeRefreshToken(String.valueOf(userId), sessionId, refreshToken)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p2] 같은 refreshToken으로 재발급 요청이 동시에 들어오면, 먼저 처리된 요청만 성공하고 나머지는 기존 토큰이 이미 삭제되어 401을 반환하는 것으로 이해했습니다!

예를 들어 accessToken 만료 시 여러 API 요청이 각각 재발급을 시도하면, 하나는 성공하더라도 다른 요청의 재발급 실패를 보고 프론트가 로그아웃 처리할 가능성이 있을 것 같습니다.

현재 프론트에서는 재발급이 진행 중일 때 다른 요청들이 그 결과를 기다리도록 처리하고 있을까요? 해당 처리가 되어 있다면 이번 방식으로도 괜찮을 것 같습니다.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다. 동시에 같은 refresh token으로 재발급 시도 시, 토큰 재사용을 막기 위해 하나만 성공 후 나머지는 401을 반환하게끔 하였습니다.

다만 이게 실제로 사용자한테 로그아웃으로 이어지는지는 프론트의 재발급 동시성 처리 방식에 달려있어서 이 부분은 프론트 측에 확인을 해봐야 알 것 같습니다. 확인 후 다시 답변 이어서 달도록 하겠습니다!!

throw new CustomException(AuthErrorCode.INVALID_REFRESH_TOKEN);
}

refreshTokenService.deleteRefreshToken(String.valueOf(userId), sessionId);

String newAccessToken = jwtTokenProvider.generateAccessToken(userId);
String newRefreshToken = jwtTokenProvider.generateRefreshToken(userId);
String newSessionId = refreshTokenService.saveRefreshToken(String.valueOf(userId), newRefreshToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import com.Timo.Timo.global.jwt.provider.JwtTokenProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

@Service
Expand All @@ -22,6 +22,17 @@ public class RefreshTokenService {

private static final String KEY_PREFIX = "refresh:";

private static final RedisScript<Long> COMPARE_AND_DELETE_SCRIPT = new DefaultRedisScript<>(
"local stored = redis.call('GET', KEYS[1]) "
+ "if stored == ARGV[1] then "
+ " redis.call('DEL', KEYS[1]) "
+ " return 1 "
+ "else "
+ " return 0 "
+ "end",
Long.class
);

public String saveRefreshToken(String userId, String refreshToken){
String sessionId = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(
Expand All @@ -33,10 +44,6 @@ public String saveRefreshToken(String userId, String refreshToken){
return sessionId;
}

public String getRefreshToken(String userId, String sessionId) {
return redisTemplate.opsForValue().get(KEY_PREFIX + userId + ":" + sessionId);
}

public void deleteRefreshToken(String userId, String sessionId) {
redisTemplate.delete(KEY_PREFIX + userId + ":" + sessionId);
}
Expand All @@ -60,7 +67,12 @@ public void deleteAllRefreshTokens(String userId) {
}
}

public boolean isRefreshTokenValid(String userId, String sessionId, String refreshToken) {
return Objects.equals(refreshToken, getRefreshToken(userId, sessionId));
public boolean validateAndConsumeRefreshToken(String userId, String sessionId, String refreshToken) {
Long result = redisTemplate.execute(
COMPARE_AND_DELETE_SCRIPT,
List.of(KEY_PREFIX + userId + ":" + sessionId),
refreshToken
);
return Long.valueOf(1L).equals(result);
}
}
22 changes: 6 additions & 16 deletions src/main/java/com/Timo/Timo/global/auth/utils/CookieUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,22 @@
public class CookieUtil {

public static ResponseCookie createCookie(String name, String value, long maxAgeSeconds, boolean secure) {
ResponseCookie.ResponseCookieBuilder builder = ResponseCookie.from(name, value)
return ResponseCookie.from(name, value)
.httpOnly(true)
.secure(secure)
.path("/api/v1/auth")
.maxAge(Duration.ofSeconds(maxAgeSeconds))
.sameSite(secure ? "None" : "Strict");

if (secure) {
builder.partitioned(true);
}

return builder.build();
.sameSite(secure ? "None" : "Strict")
.build();
Comment thread
Jy000n marked this conversation as resolved.
}

public static ResponseCookie expireCookie(String name, boolean secure) {
ResponseCookie.ResponseCookieBuilder builder = ResponseCookie.from(name, "")
return ResponseCookie.from(name, "")
.httpOnly(true)
.secure(secure)
.path("/api/v1/auth")
.maxAge(0)
.sameSite(secure ? "None" : "Strict");

if (secure) {
builder.partitioned(true);
}

return builder.build();
.sameSite(secure ? "None" : "Strict")
.build();
}
}