diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/dto/SignInReq.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/dto/SignInReq.java
index 788aa335e..2c39ff719 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/dto/SignInReq.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/dto/SignInReq.java
@@ -11,7 +11,10 @@ public record General(
String username,
@Schema(description = "비밀번호", example = "pennyway1234")
@NotBlank(message = "비밀번호를 입력해주세요")
- String password
+ String password,
+ @Schema(description = "사용자 기기 고유 식별자", example = "AA-BBB-CCC")
+ @NotBlank(message = "사용자 기기 고유 식별자를 입력해주세요")
+ String deviceId
) {
}
@@ -25,7 +28,10 @@ public record Oauth(
String idToken,
@Schema(description = "OIDC nonce")
@NotBlank(message = "OIDC nonce는 필수 입력값입니다.")
- String nonce
+ String nonce,
+ @Schema(description = "사용자 기기 고유 식별자", example = "AA-BBB-CCC")
+ @NotBlank(message = "사용자 기기 고유 식별자를 입력해주세요")
+ String deviceId
) {
}
}
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/dto/SignUpReq.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/dto/SignUpReq.java
index 3ac9a7e7f..cff22e19d 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/dto/SignUpReq.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/dto/SignUpReq.java
@@ -18,7 +18,7 @@
* 일반 회원가입 시엔 General, 소셜 회원가입 시엔 Oauth를 사용합니다.
*/
public class SignUpReq {
- public record Info(String username, String name, String password, String phone, String code) {
+ public record Info(String username, String name, String password, String phone, String code, String deviceId) {
public String password(PasswordEncoder passwordEncoder) {
return passwordEncoder.encode(password);
}
@@ -43,7 +43,7 @@ public String password() {
}
public record OauthInfo(String oauthId, String idToken, String nonce, String name, String username, String phone,
- String code) {
+ String code, String deviceId) {
public User toUser() {
return User.builder()
.username(username)
@@ -77,10 +77,13 @@ public record General(
@Schema(description = "6자리 정수 인증번호", example = "123456")
@NotBlank(message = "인증번호는 필수입니다.")
@Pattern(regexp = "^\\d{6}$", message = "인증번호는 6자리 숫자여야 합니다.")
- String code
+ String code,
+ @Schema(description = "사용자 기기 고유 식별자", example = "AA-BBB-CCC")
+ @NotBlank(message = "사용자 기기 고유 식별자를 입력해주세요")
+ String deviceId
) {
public Info toInfo() {
- return new Info(username, name, password, phone, code);
+ return new Info(username, name, password, phone, code, deviceId);
}
}
@@ -97,10 +100,13 @@ public record SyncWithOauth(
@Schema(description = "6자리 정수 인증번호", example = "123456")
@NotBlank(message = "인증번호는 필수입니다.")
@Pattern(regexp = "^\\d{6}$", message = "인증번호는 6자리 숫자여야 합니다.")
- String code
+ String code,
+ @Schema(description = "사용자 기기 고유 식별자", example = "AA-BBB-CCC")
+ @NotBlank(message = "사용자 기기 고유 식별자를 입력해주세요")
+ String deviceId
) {
public Info toInfo() {
- return new Info(null, null, password, phone, code);
+ return new Info(null, null, password, phone, code, deviceId);
}
}
@@ -130,10 +136,13 @@ public record Oauth(
@Schema(description = "6자리 정수 인증번호", example = "123456")
@NotBlank(message = "인증번호는 필수입니다.")
@Pattern(regexp = "^\\d{6}$", message = "인증번호는 6자리 숫자여야 합니다.")
- String code
+ String code,
+ @Schema(description = "사용자 기기 고유 식별자", example = "AA-BBB-CCC")
+ @NotBlank(message = "사용자 기기 고유 식별자를 입력해주세요")
+ String deviceId
) {
public OauthInfo toOauthInfo() {
- return new OauthInfo(oauthId, idToken, nonce, name, username, phone, code);
+ return new OauthInfo(oauthId, idToken, nonce, name, username, phone, code, deviceId);
}
}
@@ -155,10 +164,13 @@ public record SyncWithAuth(
@Schema(description = "6자리 정수 인증번호", example = "123456")
@NotBlank(message = "인증번호는 필수입니다.")
@Pattern(regexp = "^\\d{6}$", message = "인증번호는 6자리 숫자여야 합니다.")
- String code
+ String code,
+ @Schema(description = "사용자 기기 고유 식별자", example = "AA-BBB-CCC")
+ @NotBlank(message = "사용자 기기 고유 식별자를 입력해주세요")
+ String deviceId
) {
public OauthInfo toOauthInfo() {
- return new OauthInfo(oauthId, idToken, nonce, null, null, phone, code);
+ return new OauthInfo(oauthId, idToken, nonce, null, null, phone, code, deviceId);
}
}
}
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/helper/JwtAuthHelper.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/helper/JwtAuthHelper.java
index e17f121c9..ad528942a 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/helper/JwtAuthHelper.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/helper/JwtAuthHelper.java
@@ -46,14 +46,15 @@ public JwtAuthHelper(
* 사용자 정보 기반으로 access token과 refresh token을 생성하는 메서드
* refresh token은 redis에 저장된다.
*
- * @param user {@link User}
+ * @param user {@link User} : 사용자 정보
+ * @param deviceId String : 사용자의 디바이스 고유 식별자
* @return {@link Jwts}
*/
- public Jwts createToken(User user) {
+ public Jwts createToken(User user, String deviceId) {
String accessToken = accessTokenProvider.generateToken(AccessTokenClaim.of(user.getId(), user.getRole().getType()));
- String refreshToken = refreshTokenProvider.generateToken(RefreshTokenClaim.of(user.getId(), user.getRole().getType()));
+ String refreshToken = refreshTokenProvider.generateToken(RefreshTokenClaim.of(user.getId(), deviceId, user.getRole().getType()));
- refreshTokenService.save(RefreshToken.of(user.getId(), refreshToken, toSeconds(refreshTokenProvider.getExpiryDate(refreshToken))));
+ refreshTokenService.save(RefreshToken.of(user.getId(), deviceId, refreshToken, toSeconds(refreshTokenProvider.getExpiryDate(refreshToken))));
return Jwts.of(accessToken, refreshToken);
}
@@ -62,11 +63,12 @@ public Pair refresh(String refreshToken) {
Long userId = JwtClaimsParserUtil.getClaimsValue(claims, RefreshTokenClaimKeys.USER_ID.getValue(), Long::parseLong);
String role = JwtClaimsParserUtil.getClaimsValue(claims, RefreshTokenClaimKeys.ROLE.getValue(), String.class);
- log.debug("refresh token userId : {}, role : {}", userId, role);
+ String deviceId = JwtClaimsParserUtil.getClaimsValue(claims, RefreshTokenClaimKeys.DEVICE_ID.getValue(), String.class);
+ log.debug("refresh token userId : {}, deviceId: {}, role : {}", userId, deviceId, role);
RefreshToken newRefreshToken;
try {
- newRefreshToken = refreshTokenService.refresh(userId, refreshToken, refreshTokenProvider.generateToken(RefreshTokenClaim.of(userId, role)));
+ newRefreshToken = refreshTokenService.refresh(userId, deviceId, refreshToken, refreshTokenProvider.generateToken(RefreshTokenClaim.of(userId, deviceId, role)));
log.debug("new refresh token : {}", newRefreshToken.getToken());
} catch (IllegalArgumentException e) {
throw new JwtErrorException(JwtErrorCode.EXPIRED_TOKEN);
@@ -102,22 +104,23 @@ public void removeAccessTokenAndRefreshToken(Long userId, String accessToken, St
}
if (jwtClaims != null) {
- deleteRefreshToken(userId, jwtClaims, refreshToken);
+ deleteRefreshToken(userId, jwtClaims);
}
deleteAccessToken(userId, accessToken);
}
- private void deleteRefreshToken(Long userId, JwtClaims jwtClaims, String refreshToken) {
- Long refreshTokenUserId = Long.parseLong((String) jwtClaims.getClaims().get(RefreshTokenClaimKeys.USER_ID.getValue()));
- log.info("로그아웃 요청 refresh token id : {}", refreshTokenUserId);
+ private void deleteRefreshToken(Long userId, JwtClaims jwtClaims) {
+ Long refreshTokenUserId = JwtClaimsParserUtil.getClaimsValue(jwtClaims, RefreshTokenClaimKeys.USER_ID.getValue(), Long::parseLong);
+ String refreshTokenDeviceId = JwtClaimsParserUtil.getClaimsValue(jwtClaims, RefreshTokenClaimKeys.DEVICE_ID.getValue(), String.class);
+ log.info("로그아웃 요청 refresh token userId : {}, deviceId : {}", refreshTokenUserId, refreshTokenDeviceId);
if (!userId.equals(refreshTokenUserId)) {
throw new JwtErrorException(JwtErrorCode.WITHOUT_OWNERSHIP_REFRESH_TOKEN);
}
try {
- refreshTokenService.delete(refreshTokenUserId, refreshToken);
+ refreshTokenService.deleteAll(refreshTokenUserId);
} catch (IllegalArgumentException e) {
log.warn("refresh token not found. id : {}", userId);
}
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/usecase/AuthUseCase.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/usecase/AuthUseCase.java
index 6dc094a9e..81ddc4fd8 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/usecase/AuthUseCase.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/usecase/AuthUseCase.java
@@ -46,14 +46,14 @@ public Pair signUp(SignUpReq.Info request) {
UserSyncDto userSync = checkOauthUserNotGeneralSignUp(request.phone());
User user = userGeneralSignService.saveUserWithEncryptedPassword(request, userSync);
- return Pair.of(user.getId(), jwtAuthHelper.createToken(user));
+ return Pair.of(user.getId(), jwtAuthHelper.createToken(user, request.deviceId()));
}
@Transactional(readOnly = true)
public Pair signIn(SignInReq.General request) {
User user = userGeneralSignService.readUserIfValid(request.username(), request.password());
- return Pair.of(user.getId(), jwtAuthHelper.createToken(user));
+ return Pair.of(user.getId(), jwtAuthHelper.createToken(user, request.deviceId()));
}
public Pair refresh(String refreshToken) {
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/usecase/OauthUseCase.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/usecase/OauthUseCase.java
index d71b9939e..93a092f70 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/usecase/OauthUseCase.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/usecase/OauthUseCase.java
@@ -39,7 +39,7 @@ public Pair signIn(Provider provider, SignInReq.Oauth request) {
User user = userOauthSignService.readUser(request.oauthId(), provider);
- return (user != null) ? Pair.of(user.getId(), jwtAuthHelper.createToken(user)) : Pair.of(-1L, null);
+ return (user != null) ? Pair.of(user.getId(), jwtAuthHelper.createToken(user, request.deviceId())) : Pair.of(-1L, null);
}
@Transactional(readOnly = true)
@@ -67,7 +67,7 @@ public Pair signUp(Provider provider, SignUpReq.OauthInfo request) {
OidcDecodePayload payload = oauthOidcHelper.getPayload(provider, request.oauthId(), request.idToken(), request.nonce());
User user = userOauthSignService.saveUser(request, userSync, provider, payload.sub());
- return Pair.of(user.getId(), jwtAuthHelper.createToken(user));
+ return Pair.of(user.getId(), jwtAuthHelper.createToken(user, request.deviceId()));
}
/**
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenClaim.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenClaim.java
index 1d346e49c..4d7085d33 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenClaim.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenClaim.java
@@ -6,16 +6,16 @@
import java.util.Map;
-import static kr.co.pennyway.api.common.security.jwt.refresh.RefreshTokenClaimKeys.ROLE;
-import static kr.co.pennyway.api.common.security.jwt.refresh.RefreshTokenClaimKeys.USER_ID;
+import static kr.co.pennyway.api.common.security.jwt.refresh.RefreshTokenClaimKeys.*;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class RefreshTokenClaim implements JwtClaims {
private final Map claims;
- public static RefreshTokenClaim of(Long userId, String role) {
+ public static RefreshTokenClaim of(Long userId, String deviceToken, String role) {
Map claims = Map.of(
USER_ID.getValue(), userId.toString(),
+ DEVICE_ID.getValue(), deviceToken,
ROLE.getValue(), role
);
return new RefreshTokenClaim(claims);
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenClaimKeys.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenClaimKeys.java
index 46a9752ad..304c50301 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenClaimKeys.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenClaimKeys.java
@@ -2,7 +2,8 @@
public enum RefreshTokenClaimKeys {
USER_ID("id"),
- ROLE("role");
+ ROLE("role"),
+ DEVICE_ID("deviceId");
private final String value;
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenProvider.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenProvider.java
index 4ecdd7eda..ab7d612cf 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenProvider.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/security/jwt/refresh/RefreshTokenProvider.java
@@ -22,8 +22,7 @@
import java.util.Date;
import java.util.Map;
-import static kr.co.pennyway.api.common.security.jwt.refresh.RefreshTokenClaimKeys.ROLE;
-import static kr.co.pennyway.api.common.security.jwt.refresh.RefreshTokenClaimKeys.USER_ID;
+import static kr.co.pennyway.api.common.security.jwt.refresh.RefreshTokenClaimKeys.*;
@Slf4j
@Component
@@ -56,7 +55,11 @@ public String generateToken(JwtClaims claims) {
@Override
public JwtClaims getJwtClaimsFromToken(String token) {
Claims claims = getClaimsFromToken(token);
- return RefreshTokenClaim.of(Long.parseLong(claims.get(USER_ID.getValue(), String.class)), claims.get(ROLE.getValue(), String.class));
+ return RefreshTokenClaim.of(
+ Long.parseLong(claims.get(USER_ID.getValue(), String.class)),
+ claims.get(DEVICE_ID.getValue(), String.class),
+ claims.get(ROLE.getValue(), String.class)
+ );
}
@Override
diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/controller/AuthControllerValidationTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/controller/AuthControllerValidationTest.java
index c5ef70386..bf6feb79b 100644
--- a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/controller/AuthControllerValidationTest.java
+++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/controller/AuthControllerValidationTest.java
@@ -52,11 +52,11 @@ void setUp(WebApplicationContext webApplicationContext) {
.build();
}
- @DisplayName("[1] 아이디, 이름, 비밀번호, 전화번호, 인증번호 필수 입력")
+ @DisplayName("[1] 아이디, 이름, 비밀번호, 전화번호, 인증번호, 디바이스 아이디 필수 입력")
@Test
void requiredInputError() throws Exception {
// given
- SignUpReq.General request = new SignUpReq.General("", "", "", "", "");
+ SignUpReq.General request = new SignUpReq.General("", "", "", "", "", "");
// when
ResultActions resultActions = mockMvc.perform(
@@ -73,6 +73,7 @@ void requiredInputError() throws Exception {
.andExpect(jsonPath("$.fieldErrors.password").exists())
.andExpect(jsonPath("$.fieldErrors.phone").exists())
.andExpect(jsonPath("$.fieldErrors.code").exists())
+ .andExpect(jsonPath("$.fieldErrors.deviceId").exists())
.andDo(print());
}
@@ -81,7 +82,7 @@ void requiredInputError() throws Exception {
void idValidError() throws Exception {
// given
SignUpReq.General request = new SignUpReq.General("#pennyway", "페니웨이", "pennyway1234",
- "010-1234-5678", "123456");
+ "010-1234-5678", "123456", "AA-BBB-CCC");
// when
ResultActions resultActions = mockMvc.perform(
@@ -102,7 +103,7 @@ void idValidError() throws Exception {
void nameValidError() throws Exception {
// given
SignUpReq.General request = new SignUpReq.General("pennyway", "페니웨이12345", "pennyway1234",
- "010-1234-5678", "123456");
+ "010-1234-5678", "123456", "AA-BBB-CCC");
// when
ResultActions resultActions = mockMvc.perform(
@@ -123,7 +124,7 @@ void nameValidError() throws Exception {
void passwordValidError() throws Exception {
// given
SignUpReq.General request = new SignUpReq.General("pennyway", "페니웨이", "pennyway",
- "010-1234-5678", "123456");
+ "010-1234-5678", "123456", "AA-BBB-CCC");
// when
ResultActions resultActions = mockMvc.perform(
@@ -145,7 +146,7 @@ void passwordValidError() throws Exception {
void phoneValidError() throws Exception {
// given
SignUpReq.General request = new SignUpReq.General("pennyway", "페니웨이", "pennyway1234",
- "01012345673", "123456");
+ "01012345673", "123456", "AA-BBB-CCC");
// when
ResultActions resultActions = mockMvc.perform(
@@ -166,7 +167,7 @@ void phoneValidError() throws Exception {
void codeValidError() throws Exception {
// given
SignUpReq.General request = new SignUpReq.General("pennyway", "페니웨이", "pennyway1234",
- "010-1234-5678", "12345");
+ "010-1234-5678", "12345", "AA-BBB-CCC");
// when
ResultActions resultActions = mockMvc.perform(
@@ -187,7 +188,7 @@ void codeValidError() throws Exception {
void someFieldMissingError() throws Exception {
// given
SignUpReq.General request = new SignUpReq.General("pennyway", "페니웨이", "pennyway1234",
- "010-1234-5678", "123456");
+ "010-1234-5678", "123456", "AA-BBB-CCC");
// when
ResultActions resultActions = mockMvc.perform(
@@ -210,7 +211,7 @@ void someFieldMissingError() throws Exception {
void signUp() throws Exception {
// given
SignUpReq.General request = new SignUpReq.General("pennyway123", "페니웨이", "pennyway1234",
- "010-1234-5678", "123456");
+ "010-1234-5678", "123456", "AA-BBB-CCC");
ResponseCookie expectedCookie = ResponseCookie.from("refreshToken", "refreshToken")
.maxAge(Duration.ofDays(7).toSeconds()).httpOnly(true).path("/").build();
diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/helper/JwtAuthHelperTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/helper/JwtAuthHelperTest.java
index bc9ac2cdf..301804f84 100644
--- a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/helper/JwtAuthHelperTest.java
+++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/helper/JwtAuthHelperTest.java
@@ -61,11 +61,12 @@ public void RefreshTokenRefreshSuccess() {
// given
RefreshToken refreshToken = RefreshToken.builder()
.userId(1L)
+ .deviceId("AA-BBB-CC-DDD")
.token("refreshToken")
.ttl(1000L)
.build();
refreshTokenRepository.save(refreshToken);
- given(refreshTokenProvider.getJwtClaimsFromToken(refreshToken.getToken())).willReturn(RefreshTokenClaim.of(refreshToken.getUserId(), Role.USER.getType()));
+ given(refreshTokenProvider.getJwtClaimsFromToken(refreshToken.getToken())).willReturn(RefreshTokenClaim.of(refreshToken.getUserId(), refreshToken.getDeviceId(), Role.USER.getType()));
given(accessTokenProvider.generateToken(any())).willReturn("newAccessToken");
given(refreshTokenProvider.generateToken(any())).willReturn("newRefreshToken");
@@ -76,7 +77,7 @@ public void RefreshTokenRefreshSuccess() {
assertEquals("사용자 아이디가 일치하지 않습니다.", refreshToken.getUserId(), jwts.getLeft());
assertEquals("갱신된 액세스 토큰이 일치하지 않습니다.", "newAccessToken", jwts.getRight().accessToken());
assertEquals("리프레시 토큰이 갱신되지 않았습니다.", "newRefreshToken", jwts.getRight().refreshToken());
- log.info("갱신된 리프레시 토큰 정보 : {}", refreshTokenRepository.findById(refreshToken.getUserId()).orElse(null));
+ log.info("갱신된 리프레시 토큰 정보 : {}", refreshTokenRepository.findById(refreshToken.getId()).orElse(null));
}
@Test
@@ -85,12 +86,13 @@ public void RefreshTokenRefreshFail() {
// given
RefreshToken refreshToken = RefreshToken.builder()
.userId(1L)
+ .deviceId("AA-BBB-CC-DDD")
.token("refreshToken")
.ttl(1000L)
.build();
refreshTokenRepository.save(refreshToken);
- given(refreshTokenProvider.getJwtClaimsFromToken("anotherRefreshToken")).willReturn(RefreshTokenClaim.of(refreshToken.getUserId(), Role.USER.toString()));
+ given(refreshTokenProvider.getJwtClaimsFromToken("anotherRefreshToken")).willReturn(RefreshTokenClaim.of(refreshToken.getUserId(), refreshToken.getDeviceId(), Role.USER.toString()));
given(refreshTokenProvider.generateToken(any())).willReturn("newRefreshToken");
// when
@@ -98,6 +100,6 @@ public void RefreshTokenRefreshFail() {
// then
assertEquals("탈취 시나리오 예외가 발생하지 않았습니다.", JwtErrorCode.TAKEN_AWAY_TOKEN, jwtErrorException.getErrorCode());
- assertFalse("리프레시 토큰이 삭제되지 않았습니다.", refreshTokenRepository.existsById(refreshToken.getUserId()));
+ assertFalse("리프레시 토큰이 삭제되지 않았습니다.", refreshTokenRepository.existsById(refreshToken.getId()));
}
}
diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/AuthControllerIntegrationTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/AuthControllerIntegrationTest.java
index 30c2f5a32..33c1a9a1f 100644
--- a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/AuthControllerIntegrationTest.java
+++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/AuthControllerIntegrationTest.java
@@ -43,6 +43,7 @@
public class AuthControllerIntegrationTest extends ExternalApiDBTestConfig {
private final String expectedPhone = "010-1234-5678";
private final String expectedCode = "123456";
+ private final String expectedDeviceId = "AA-BB-CC-DD";
@Autowired
private MockMvc mockMvc;
@@ -224,7 +225,7 @@ void generalSignUpSuccess() throws Exception {
}
private ResultActions performGeneralSignUpRequest(String code) throws Exception {
- SignUpReq.General request = new SignUpReq.General(UserFixture.GENERAL_USER.getUsername(), "pennyway", "dkssudgktpdy1", expectedPhone, code);
+ SignUpReq.General request = new SignUpReq.General(UserFixture.GENERAL_USER.getUsername(), "pennyway", "dkssudgktpdy1", expectedPhone, code, expectedDeviceId);
return mockMvc.perform(
post("/v1/auth/sign-up")
.contentType(MediaType.APPLICATION_JSON)
@@ -286,7 +287,7 @@ void syncWithOauthSignUpSuccess() throws Exception {
}
private ResultActions performSyncWithOauthSignUpRequest(String expectedPhone, String code) throws Exception {
- SignUpReq.SyncWithOauth request = new SignUpReq.SyncWithOauth("dkssudgktpdy1", expectedPhone, code);
+ SignUpReq.SyncWithOauth request = new SignUpReq.SyncWithOauth("dkssudgktpdy1", expectedPhone, code, expectedDeviceId);
return mockMvc.perform(
post("/v1/auth/link-oauth")
.contentType(MediaType.APPLICATION_JSON)
diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/OAuthControllerIntegrationTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/OAuthControllerIntegrationTest.java
index af0bdc4db..977efc062 100644
--- a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/OAuthControllerIntegrationTest.java
+++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/OAuthControllerIntegrationTest.java
@@ -64,6 +64,7 @@ public class OAuthControllerIntegrationTest extends ExternalApiDBTestConfig {
private final String expectedNonce = "testNonce";
private final String expectedPhone = "010-1234-5678";
private final String expectedCode = "123456";
+ private final String expectedDeviceId = "testDeviceId";
@Autowired
private MockMvc mockMvc;
@Autowired
@@ -226,7 +227,7 @@ void signInWithNoSignedUser() throws Exception {
}
private ResultActions performOauthSignIn(Provider provider, String oauthId, String idToken, String nonce) throws Exception {
- SignInReq.Oauth request = new SignInReq.Oauth(oauthId, idToken, expectedNonce);
+ SignInReq.Oauth request = new SignInReq.Oauth(oauthId, idToken, expectedNonce, expectedDeviceId);
return mockMvc.perform(post("/v1/auth/oauth/sign-in")
.param("provider", provider.name())
@@ -557,7 +558,7 @@ void signUpWithDeletedOauth() throws Exception {
}
private ResultActions performOauthSignUpAccountLinking(Provider provider, String code, String oauthId) throws Exception {
- SignUpReq.SyncWithAuth request = new SignUpReq.SyncWithAuth(oauthId, expectedIdToken, expectedNonce, expectedPhone, code);
+ SignUpReq.SyncWithAuth request = new SignUpReq.SyncWithAuth(oauthId, expectedIdToken, expectedNonce, expectedPhone, code, expectedDeviceId);
return mockMvc.perform(post("/v1/auth/oauth/link-auth")
.param("provider", provider.name())
.contentType("application/json")
@@ -645,7 +646,7 @@ void signUpWithOauthSignedUser() throws Exception {
}
private ResultActions performOauthSignUp(Provider provider, String code) throws Exception {
- SignUpReq.Oauth request = new SignUpReq.Oauth(expectedOauthId, expectedIdToken, expectedNonce, "jayang", expectedUsername, expectedPhone, code);
+ SignUpReq.Oauth request = new SignUpReq.Oauth(expectedOauthId, expectedIdToken, expectedNonce, "jayang", expectedUsername, expectedPhone, code, expectedDeviceId);
return mockMvc.perform(post("/v1/auth/oauth/sign-up")
.param("provider", provider.name())
.contentType("application/json")
diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/UserAuthControllerIntegrationTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/UserAuthControllerIntegrationTest.java
index 62bf9599b..091b62257 100644
--- a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/UserAuthControllerIntegrationTest.java
+++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/auth/integration/UserAuthControllerIntegrationTest.java
@@ -83,6 +83,7 @@ public class UserAuthControllerIntegrationTest extends ExternalApiDBTestConfig {
class SignOut {
private String expectedAccessToken;
private String expectedRefreshToken;
+ private String expectedDeviceId;
private Long userId;
@BeforeEach
@@ -90,15 +91,16 @@ void setUp() {
User user = UserFixture.GENERAL_USER.toUser();
userService.createUser(user);
userId = user.getId();
+ expectedDeviceId = "AA-BBB-CC-DDD";
expectedAccessToken = accessTokenProvider.generateToken(AccessTokenClaim.of(user.getId(), Role.USER.getType()));
- expectedRefreshToken = refreshTokenProvider.generateToken(RefreshTokenClaim.of(user.getId(), Role.USER.getType()));
+ expectedRefreshToken = refreshTokenProvider.generateToken(RefreshTokenClaim.of(user.getId(), expectedDeviceId, Role.USER.getType()));
}
@Test
@DisplayName("Scenario #1 유효한 accessToken과 refreshToken이 있다면, accessToken은 forbiddenToken으로, refreshToken은 삭제한다.")
void validAccessTokenAndValidRefreshToken() throws Exception {
// given
- refreshTokenService.save(RefreshToken.of(userId, expectedRefreshToken, refreshTokenProvider.getExpiryDate(expectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
+ refreshTokenService.save(RefreshToken.of(userId, expectedDeviceId, expectedRefreshToken, refreshTokenProvider.getExpiryDate(expectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
// when
ResultActions result = mockMvc.perform(performSignOut()
@@ -108,7 +110,7 @@ void validAccessTokenAndValidRefreshToken() throws Exception {
// then
result.andExpect(status().isOk()).andDo(print());
assertTrue(forbiddenTokenService.isForbidden(expectedAccessToken));
- assertThrows(IllegalArgumentException.class, () -> refreshTokenService.delete(userId, expectedRefreshToken));
+ refreshTokenService.deleteAll(userId);
}
@Test
@@ -126,9 +128,10 @@ void validAccessTokenWithoutRefreshToken() throws Exception {
@DisplayName("Scenario #2-1 유효한 accessToken과 다른 사용자의 유효한 refreshToken이 있다면, 401 에러를 반환한다. accessToken이 forbidden 처리되지 않으며, 사용자와 다른 사용자의 refreshToken 정보 모두 삭제되지 않는다.")
void validAccessTokenAndWithOutOwnershipRefreshToken() throws Exception {
// given
- String unexpectedRefreshToken = refreshTokenProvider.generateToken(RefreshTokenClaim.of(1000L, Role.USER.getType()));
- refreshTokenService.save(RefreshToken.of(userId, expectedRefreshToken, refreshTokenProvider.getExpiryDate(expectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
- refreshTokenService.save(RefreshToken.of(1000L, unexpectedRefreshToken, refreshTokenProvider.getExpiryDate(unexpectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
+ String otherDeviceId = "BB-CCC-DDD";
+ String unexpectedRefreshToken = refreshTokenProvider.generateToken(RefreshTokenClaim.of(1000L, otherDeviceId, Role.USER.getType()));
+ refreshTokenService.save(RefreshToken.of(userId, expectedDeviceId, expectedRefreshToken, refreshTokenProvider.getExpiryDate(expectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
+ refreshTokenService.save(RefreshToken.of(1000L, otherDeviceId, unexpectedRefreshToken, refreshTokenProvider.getExpiryDate(unexpectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
// when
ResultActions result = mockMvc
@@ -140,9 +143,10 @@ void validAccessTokenAndWithOutOwnershipRefreshToken() throws Exception {
.andExpect(jsonPath("$.code").value(JwtErrorCode.WITHOUT_OWNERSHIP_REFRESH_TOKEN.causedBy().getCode()))
.andExpect(jsonPath("$.message").value(JwtErrorCode.WITHOUT_OWNERSHIP_REFRESH_TOKEN.getExplainError()))
.andDo(print());
- assertDoesNotThrow(() -> refreshTokenService.delete(userId, expectedRefreshToken));
- assertDoesNotThrow(() -> refreshTokenService.delete(1000L, unexpectedRefreshToken));
assertFalse(forbiddenTokenService.isForbidden(expectedAccessToken));
+
+ refreshTokenService.deleteAll(userId);
+ refreshTokenService.deleteAll(1000L);
}
@Test
@@ -150,7 +154,7 @@ void validAccessTokenAndWithOutOwnershipRefreshToken() throws Exception {
void validAccessTokenAndInvalidRefreshToken() throws Exception {
// given
long ttl = refreshTokenProvider.getExpiryDate(expectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
- refreshTokenService.save(RefreshToken.of(userId, expectedRefreshToken, ttl));
+ refreshTokenService.save(RefreshToken.of(userId, expectedDeviceId, expectedRefreshToken, ttl));
// when
ResultActions result = mockMvc.perform(performSignOut()
@@ -163,16 +167,17 @@ void validAccessTokenAndInvalidRefreshToken() throws Exception {
.andExpect(jsonPath("$.code").value(JwtErrorCode.MALFORMED_TOKEN.causedBy().getCode()))
.andExpect(jsonPath("$.message").value(JwtErrorCode.MALFORMED_TOKEN.getExplainError()))
.andDo(print());
- assertDoesNotThrow(() -> refreshTokenService.delete(userId, expectedRefreshToken));
assertFalse(forbiddenTokenService.isForbidden(expectedAccessToken));
+
+ refreshTokenService.deleteAll(userId);
}
@Test
@DisplayName("Scenario #2-3 유효한 accessToken, 유효한 refreshToken을 가진 사용자가 refresh 하기 전의 refreshToken을 사용하는 경우, accessToken을 forbidden에 등록하고 refreshToken을 cache에서 제거한다. (refreshToken 탈취 대체 시나리오)")
void validAccessTokenAndOldRefreshToken() throws Exception {
// given
- String oldRefreshToken = refreshTokenProvider.generateToken(RefreshTokenClaim.of(userId, Role.USER.getType()));
- refreshTokenService.save(RefreshToken.of(userId, expectedRefreshToken, refreshTokenProvider.getExpiryDate(expectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
+ String oldRefreshToken = refreshTokenProvider.generateToken(RefreshTokenClaim.of(userId, expectedDeviceId, Role.USER.getType()));
+ refreshTokenService.save(RefreshToken.of(userId, expectedDeviceId, expectedRefreshToken, refreshTokenProvider.getExpiryDate(expectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
// when
ResultActions result = mockMvc.perform(performSignOut()
@@ -184,16 +189,16 @@ void validAccessTokenAndOldRefreshToken() throws Exception {
.andExpect(status().isOk())
.andExpect(header().exists(HttpHeaders.SET_COOKIE))
.andDo(print());
- assertThrows(IllegalArgumentException.class, () -> refreshTokenService.delete(userId, oldRefreshToken));
- assertThrows(IllegalArgumentException.class, () -> refreshTokenService.delete(userId, expectedRefreshToken));
assertTrue(forbiddenTokenService.isForbidden(expectedAccessToken));
+
+ refreshTokenService.deleteAll(userId);
}
@Test
@DisplayName("Scenario #3 유효하지 않은 accessToken과 유효한 refreshToken이 있다면 401 에러를 반환한다.")
void invalidAccessTokenAndValidRefreshToken() throws Exception {
// given
- refreshTokenService.save(RefreshToken.of(userId, expectedRefreshToken, refreshTokenProvider.getExpiryDate(expectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
+ refreshTokenService.save(RefreshToken.of(userId, expectedDeviceId, expectedRefreshToken, refreshTokenProvider.getExpiryDate(expectedRefreshToken).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
// when
ResultActions result = mockMvc.perform(performSignOut()
@@ -335,7 +340,7 @@ void linkOauthWithDeletedOauth() throws Exception {
private ResultActions performLinkOauth(Provider provider, String oauthId, User requestUser) throws Exception {
UserDetails userDetails = SecurityUserDetails.from(requestUser);
- SignInReq.Oauth request = new SignInReq.Oauth(oauthId, "idToken", "nonce");
+ SignInReq.Oauth request = new SignInReq.Oauth(oauthId, "idToken", "nonce", "deviceId");
return mockMvc.perform(put("/v1/link-oauth")
.contentType(MediaType.APPLICATION_JSON)
diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshToken.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshToken.java
index db38f7a4c..b8689145d 100644
--- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshToken.java
+++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshToken.java
@@ -1,9 +1,6 @@
package kr.co.pennyway.domain.common.redis.refresh;
-import lombok.Builder;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.ToString;
+import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@@ -11,27 +8,37 @@
@Getter
@ToString(of = {"userId", "token", "ttl"})
@EqualsAndHashCode(of = {"userId", "token"})
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshToken {
@Id
- private final Long userId;
- private final long ttl;
+ private String id;
+ private Long userId;
+ private String deviceId;
+ private long ttl;
private String token;
@Builder
- private RefreshToken(String token, Long userId, long ttl) {
- this.token = token;
+ private RefreshToken(Long userId, String deviceId, String token, long ttl) {
+ this.id = createId(userId, deviceId);
this.userId = userId;
+ this.deviceId = deviceId;
+ this.token = token;
this.ttl = ttl;
}
- public static RefreshToken of(Long userId, String token, long ttl) {
+ public static RefreshToken of(Long userId, String deviceId, String token, long ttl) {
return RefreshToken.builder()
.userId(userId)
+ .deviceId(deviceId)
.token(token)
.ttl(ttl)
.build();
}
+ public static String createId(Long userId, String deviceId) {
+ return userId + ":" + deviceId;
+ }
+
protected void rotation(String token) {
this.token = token;
}
diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenCustomRepository.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenCustomRepository.java
new file mode 100644
index 000000000..a23ac6924
--- /dev/null
+++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenCustomRepository.java
@@ -0,0 +1,5 @@
+package kr.co.pennyway.domain.common.redis.refresh;
+
+public interface RefreshTokenCustomRepository {
+ void deleteAllByUserId(Long userId);
+}
diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenCustomRepositoryImpl.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenCustomRepositoryImpl.java
new file mode 100644
index 000000000..2bed8a805
--- /dev/null
+++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenCustomRepositoryImpl.java
@@ -0,0 +1,23 @@
+package kr.co.pennyway.domain.common.redis.refresh;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.util.Set;
+
+@Repository
+@RequiredArgsConstructor
+public class RefreshTokenCustomRepositoryImpl implements RefreshTokenCustomRepository {
+ private final RedisTemplate redisTemplate;
+
+ @Override
+ public void deleteAllByUserId(Long userId) {
+ String pattern = "refreshToken:" + userId + ":*";
+ Set keys = redisTemplate.keys(pattern);
+
+ if (keys != null && !keys.isEmpty()) {
+ redisTemplate.delete(keys);
+ }
+ }
+}
diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenRepository.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenRepository.java
index 35467af12..a7e68cd3e 100644
--- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenRepository.java
+++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenRepository.java
@@ -2,5 +2,5 @@
import org.springframework.data.repository.CrudRepository;
-public interface RefreshTokenRepository extends CrudRepository {
+public interface RefreshTokenRepository extends CrudRepository, RefreshTokenCustomRepository {
}
\ No newline at end of file
diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenService.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenService.java
index 98158f8b7..e53b41f0d 100644
--- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenService.java
+++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenService.java
@@ -12,20 +12,19 @@ public interface RefreshTokenService {
* 사용자가 보낸 refresh token으로 기존 refresh token과 비교 검증 후, 새로운 refresh token으로 저장한다.
*
* @param userId : 토큰 주인 pk
+ * @param deviceId : 토큰 발급한 디바이스
* @param oldRefreshToken : 사용자가 보낸 refresh token
* @param newRefreshToken : 교체할 refresh token
* @return {@link RefreshToken}
* @throws IllegalArgumentException : userId에 해당하는 refresh token이 없을 경우
* @throws IllegalStateException : 요청한 토큰과 저장된 토큰이 다르다면 토큰이 탈취되었다고 판단하여 값 삭제
*/
- RefreshToken refresh(Long userId, String oldRefreshToken, String newRefreshToken) throws IllegalArgumentException, IllegalStateException;
+ RefreshToken refresh(Long userId, String deviceId, String oldRefreshToken, String newRefreshToken) throws IllegalArgumentException, IllegalStateException;
/**
- * access token 으로 refresh token을 찾아서 제거 (로그아웃)
+ * 사용자에게 할당된 모든 Device의 refresh token을 삭제한다.
*
- * @param userId : 토큰 주인 pk
- * @param refreshToken : 검증용 refresh token
- * @throws IllegalArgumentException : userId에 해당하는 refresh token이 없을 경우
+ * @param userId : 토큰 주인 pk
*/
- void delete(Long userId, String refreshToken) throws IllegalArgumentException;
+ void deleteAll(Long userId);
}
diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenServiceImpl.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenServiceImpl.java
index 194ed90b9..d7eff2755 100644
--- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenServiceImpl.java
+++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenServiceImpl.java
@@ -9,7 +9,7 @@
@RequiredArgsConstructor
public class RefreshTokenServiceImpl implements RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;
-
+
@Override
public void save(RefreshToken refreshToken) {
refreshTokenRepository.save(refreshToken);
@@ -17,8 +17,8 @@ public void save(RefreshToken refreshToken) {
}
@Override
- public RefreshToken refresh(Long userId, String oldRefreshToken, String newRefreshToken) throws IllegalArgumentException, IllegalStateException {
- RefreshToken refreshToken = findOrElseThrow(userId);
+ public RefreshToken refresh(Long userId, String deviceId, String oldRefreshToken, String newRefreshToken) throws IllegalArgumentException, IllegalStateException {
+ RefreshToken refreshToken = findOrElseThrow(userId, deviceId);
validateToken(oldRefreshToken, refreshToken);
@@ -30,14 +30,13 @@ public RefreshToken refresh(Long userId, String oldRefreshToken, String newRefre
}
@Override
- public void delete(Long userId, String refreshToken) throws IllegalArgumentException {
- RefreshToken token = findOrElseThrow(userId);
- refreshTokenRepository.delete(token);
+ public void deleteAll(Long userId) {
+ refreshTokenRepository.deleteAllByUserId(userId);
log.info("사용자 {}의 리프레시 토큰 삭제", userId);
}
- private RefreshToken findOrElseThrow(Long userId) {
- return refreshTokenRepository.findById(userId)
+ private RefreshToken findOrElseThrow(Long userId, String deviceId) {
+ return refreshTokenRepository.findById(RefreshToken.createId(userId, deviceId))
.orElseThrow(() -> new IllegalArgumentException("refresh token not found"));
}
@@ -49,7 +48,7 @@ private RefreshToken findOrElseThrow(Long userId) {
private void validateToken(String requestRefreshToken, RefreshToken expectedRefreshToken) throws IllegalStateException {
if (isTakenAway(requestRefreshToken, expectedRefreshToken.getToken())) {
log.warn("리프레시 토큰 불일치(탈취). expected : {}, actual : {}", requestRefreshToken, expectedRefreshToken.getToken());
- refreshTokenRepository.delete(expectedRefreshToken);
+ refreshTokenRepository.deleteAllByUserId(expectedRefreshToken.getUserId());
log.info("사용자 {}의 리프레시 토큰 삭제", expectedRefreshToken.getUserId());
throw new IllegalStateException("refresh token mismatched");
diff --git a/pennyway-domain/src/test/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenServiceIntegrationTest.java b/pennyway-domain/src/test/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenServiceIntegrationTest.java
index 204755be6..6c93dcba4 100644
--- a/pennyway-domain/src/test/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenServiceIntegrationTest.java
+++ b/pennyway-domain/src/test/java/kr/co/pennyway/domain/common/redis/refresh/RefreshTokenServiceIntegrationTest.java
@@ -11,6 +11,7 @@
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertFalse;
@@ -35,6 +36,7 @@ void saveTest() {
// given
RefreshToken refreshToken = RefreshToken.builder()
.userId(1L)
+ .deviceId("AA-BBB-CC-DDD")
.token("refreshToken")
.ttl(1000L)
.build();
@@ -43,7 +45,7 @@ void saveTest() {
refreshTokenService.save(refreshToken);
// then
- RefreshToken savedRefreshToken = refreshTokenRepository.findById(1L).orElse(null);
+ RefreshToken savedRefreshToken = refreshTokenRepository.findById(refreshToken.getId()).orElse(null);
assertEquals("저장된 리프레시 토큰이 일치하지 않습니다.", refreshToken, savedRefreshToken);
log.info("저장된 리프레시 토큰 정보 : {}", savedRefreshToken);
}
@@ -54,16 +56,17 @@ void refreshTest() {
// given
RefreshToken refreshToken = RefreshToken.builder()
.userId(1L)
+ .deviceId("AA-BBB-CC-DDD")
.token("refreshToken")
.ttl(1000L)
.build();
refreshTokenService.save(refreshToken);
// when
- refreshTokenService.refresh(1L, "refreshToken", "newRefreshToken");
+ refreshTokenService.refresh(refreshToken.getUserId(), refreshToken.getDeviceId(), refreshToken.getToken(), "newRefreshToken");
// then
- RefreshToken savedRefreshToken = refreshTokenRepository.findById(1L).orElse(null);
+ RefreshToken savedRefreshToken = refreshTokenRepository.findById(refreshToken.getId()).orElse(null);
assertEquals("갱신된 리프레시 토큰이 일치하지 않습니다.", "newRefreshToken", savedRefreshToken.getToken());
log.info("갱신된 리프레시 토큰 정보 : {}", savedRefreshToken);
}
@@ -74,16 +77,51 @@ void validateTokenTest() {
// given
RefreshToken refreshToken = RefreshToken.builder()
.userId(1L)
+ .deviceId("AA-BBB-CC-DDD")
.token("refreshToken")
.ttl(1000L)
.build();
refreshTokenService.save(refreshToken);
// when
- IllegalStateException exception = assertThrows(IllegalStateException.class, () -> refreshTokenService.refresh(1L, "anotherRefreshToken", "newRefreshToken"));
+ IllegalStateException exception = assertThrows(IllegalStateException.class, () -> refreshTokenService.refresh(refreshToken.getUserId(), refreshToken.getDeviceId(), "anotherRefreshToken", "newRefreshToken"));
// then
assertEquals("리프레시 토큰이 탈취되었을 때 예외가 발생해야 합니다.", "refresh token mismatched", exception.getMessage());
- assertFalse("리프레시 토큰이 탈취되었을 때 저장된 리프레시 토큰이 삭제되어야 합니다.", refreshTokenRepository.existsById(1L));
+ assertFalse("리프레시 토큰이 탈취되었을 때 저장된 리프레시 토큰이 삭제되어야 합니다.", refreshTokenRepository.existsById(refreshToken.getId()));
+ }
+
+ @Test
+ @DisplayName("사용자에게 할당된 모든 Device의 리프레시 토큰 삭제 테스트")
+ void deleteAllTest() {
+ // given
+ RefreshToken refreshToken1 = RefreshToken.builder()
+ .userId(1L)
+ .deviceId("AA-BBB-CC-DDD")
+ .token("refreshToken1")
+ .ttl(1000L)
+ .build();
+ RefreshToken refreshToken2 = RefreshToken.builder()
+ .userId(1L)
+ .deviceId("AA-BBB-CC-EEE")
+ .token("refreshToken2")
+ .ttl(1000L)
+ .build();
+ refreshTokenService.save(refreshToken1);
+ refreshTokenService.save(refreshToken2);
+
+ // when
+ refreshTokenService.deleteAll(refreshToken1.getUserId());
+
+ // then
+ assertFalse("사용자에게 할당된 모든 Device의 리프레시 토큰이 삭제되어야 합니다.", refreshTokenRepository.existsById(refreshToken1.getId()));
+ assertFalse("사용자에게 할당된 모든 Device의 리프레시 토큰이 삭제되어야 합니다.", refreshTokenRepository.existsById(refreshToken2.getId()));
+ }
+
+ @Test
+ @DisplayName("userId에 해당하는 리프레시 토큰이 없어도, 삭제 수행에서 예외가 발생하지 않아야 합니다.")
+ void deleteAllWithoutRefreshTokenTest() {
+ // when - then
+ assertDoesNotThrow(() -> refreshTokenService.deleteAll(1L));
}
}