diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 9c84e8d22..7fe543bd7 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -3,6 +3,7 @@ import com.example.solidconnection.auth.dto.EmailSignInRequest; import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest; import com.example.solidconnection.auth.dto.EmailSignUpTokenResponse; +import com.example.solidconnection.auth.dto.ReissueRequest; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; @@ -114,13 +115,9 @@ public ResponseEntity quit( @PostMapping("/reissue") public ResponseEntity reissueToken( - Authentication authentication + ReissueRequest reissueRequest ) { - String token = authentication.getCredentials().toString(); - if (token == null) { - throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); - } - ReissueResponse reissueResponse = authService.reissue(token); + ReissueResponse reissueResponse = authService.reissue(reissueRequest); return ResponseEntity.ok(reissueResponse); } } diff --git a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java index caf1c7a9d..560b0e139 100644 --- a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java +++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java @@ -5,16 +5,16 @@ @Getter public enum TokenType { - ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour - REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days + ACCESS("ACCESS:", 1000L * 60 * 60), // 1hour + REFRESH("REFRESH:", 1000L * 60 * 60 * 24 * 90), // 90days BLACKLIST("BLACKLIST:", ACCESS.expireTime), - SIGN_UP("SIGN_UP:", 1000 * 60 * 10), // 10min + SIGN_UP("SIGN_UP:", 1000L * 60 * 10), // 10min ; private final String prefix; - private final int expireTime; + private final long expireTime; - TokenType(String prefix, int expireTime) { + TokenType(String prefix, long expireTime) { this.prefix = prefix; this.expireTime = expireTime; } diff --git a/src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java b/src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java new file mode 100644 index 000000000..00443255d --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ReissueRequest( + @NotBlank(message = "리프레시 토큰과 함께 요청해주세요.") + String refreshToken) { +} diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 04bcadde7..289893dbf 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -1,7 +1,9 @@ package com.example.solidconnection.auth.service; +import com.example.solidconnection.auth.dto.ReissueRequest; import com.example.solidconnection.auth.dto.ReissueResponse; +import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; @@ -9,15 +11,18 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.util.Objects; import java.util.Optional; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; +import static com.example.solidconnection.util.JwtUtils.parseSubject; @RequiredArgsConstructor @Service public class AuthService { private final AuthTokenProvider authTokenProvider; + private final JwtProperties jwtProperties; /* * 로그아웃 한다. @@ -40,13 +45,15 @@ public void quit(SiteUser siteUser) { /* * 액세스 토큰을 재발급한다. - * - 리프레시 토큰이 만료되었거나, 존재하지 않는다면 예외 응답을 반환한다. - * - 리프레시 토큰이 존재한다면, 액세스 토큰을 재발급한다. + * - 요청된 리프레시 토큰과 동일한 subject 의 토큰이 저장되어 있으며 값이 일치할 경우, 액세스 토큰을 재발급한다. + * - 그렇지 않으면 예외를 반환한다. * */ - public ReissueResponse reissue(String subject) { - // 리프레시 토큰 만료 확인 - Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); - if (optionalRefreshToken.isEmpty()) { + public ReissueResponse reissue(ReissueRequest reissueRequest) { + // 리프레시 토큰 확인 + String requestedRefreshToken = reissueRequest.refreshToken(); + String subject = parseSubject(requestedRefreshToken, jwtProperties.secret()); + Optional savedRefreshToken = authTokenProvider.findRefreshToken(subject); + if (!Objects.equals(requestedRefreshToken, savedRefreshToken.orElse(null))) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java index da040a8d5..b682a4b39 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -8,8 +8,6 @@ import java.util.Optional; -import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; - @Component public class AuthTokenProvider extends TokenProvider { @@ -18,7 +16,7 @@ public AuthTokenProvider(JwtProperties jwtProperties, RedisTemplate findBlackListToken(String subject) { return Optional.ofNullable(redisTemplate.opsForValue().get(blackListTokenKey)); } - public String getEmail(String token) { - return parseSubjectIgnoringExpiration(token, jwtProperties.secret()); + private String getSubject(SiteUser siteUser) { + return siteUser.getId().toString(); } } diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java index d3ea8fed9..d295f2a3e 100644 --- a/src/main/java/com/example/solidconnection/util/JwtUtils.java +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -28,19 +28,19 @@ public static String parseTokenFromRequest(HttpServletRequest request) { return token.substring(TOKEN_PREFIX.length()); } - public static String parseSubjectIgnoringExpiration(String token, String secretKey) { + public static String parseSubject(String token, String secretKey) { try { return parseClaims(token, secretKey).getSubject(); - } catch (ExpiredJwtException e) { - return e.getClaims().getSubject(); } catch (Exception e) { throw new CustomException(INVALID_TOKEN); } } - public static String parseSubject(String token, String secretKey) { + public static String parseSubjectIgnoringExpiration(String token, String secretKey) { try { return parseClaims(token, secretKey).getSubject(); + } catch (ExpiredJwtException e) { + return e.getClaims().getSubject(); } catch (Exception e) { throw new CustomException(INVALID_TOKEN); } diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java new file mode 100644 index 000000000..0030e45e2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -0,0 +1,108 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.dto.ReissueRequest; +import com.example.solidconnection.auth.dto.ReissueResponse; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.util.JwtUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; + +import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +@DisplayName("인증 서비스 테스트") +@TestContainerSpringBootTest +class AuthServiceTest { + + @Autowired + private AuthService authService; + + @Autowired + private AuthTokenProvider authTokenProvider; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 로그아웃한다() { + // given + String accessToken = "accessToken"; + + // when + authService.signOut(accessToken); + + // then + assertThat(authTokenProvider.findBlackListToken(accessToken)).isNotNull(); + } + + @Test + void 탈퇴한다() { + // given + SiteUser siteUser = createSiteUser(); + + // when + authService.quit(siteUser); + + // then + LocalDate tomorrow = LocalDate.now().plusDays(1); + assertThat(siteUser.getQuitedAt()).isEqualTo(tomorrow); + } + + @Nested + class 토큰을_재발급한다 { + + @Test + void 요청의_리프레시_토큰이_저장되어_있고_값이_일치면_액세스_토큰을_재발급한다() { + // given + SiteUser siteUser = createSiteUser(); + String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); + ReissueRequest reissueRequest = new ReissueRequest(refreshToken); + + // when + ReissueResponse reissuedAccessToken = authService.reissue(reissueRequest); + + // then + String actualSubject = JwtUtils.parseSubject(reissuedAccessToken.accessToken(), jwtProperties.secret()); + String expectedSubject = JwtUtils.parseSubject(refreshToken, jwtProperties.secret()); + assertThat(actualSubject).isEqualTo(expectedSubject); + } + + @Test + void 요청의_리프레시_토큰이_저장되어있지_않다면_예외_응답을_반환한다() { + // given + String refreshToken = authTokenProvider.generateToken("subject", TokenType.REFRESH); + ReissueRequest reissueRequest = new ReissueRequest(refreshToken); + + // when, then + assertThatCode(() -> authService.reissue(reissueRequest)) + .isInstanceOf(CustomException.class) + .hasMessage(REFRESH_TOKEN_EXPIRED.getMessage()); + } + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + PreparationStatus.CONSIDERING, + Role.MENTEE + ); + return siteUserRepository.save(siteUser); + } +}