Skip to content
Merged
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
Expand Up @@ -3,7 +3,6 @@
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;
Expand All @@ -19,6 +18,7 @@
import com.example.solidconnection.common.exception.ErrorCode;
import com.example.solidconnection.common.resolver.AuthorizedUser;
import com.example.solidconnection.siteuser.domain.AuthType;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -118,10 +118,10 @@ public ResponseEntity<Void> quit(

@PostMapping("/reissue")
public ResponseEntity<ReissueResponse> reissueToken(
@AuthorizedUser long siteUserId,
@Valid @RequestBody ReissueRequest reissueRequest
HttpServletRequest request
) {
ReissueResponse reissueResponse = authService.reissue(siteUserId, reissueRequest);
String refreshToken = refreshTokenCookieManager.getRefreshToken(request);
ReissueResponse reissueResponse = authService.reissue(refreshToken);
return ResponseEntity.ok(reissueResponse);
Comment on lines 119 to 125
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

3) CSRF/CORS 관점에서 재발급 엔드포인트 보호 상태를 확인해 주세요.

SameSite=None + Secure로 운영 시, CORS 정책이 빈틈없이 설정되었는지와 CSRF 방어(예: Double Submit, 전용 헤더) 전략이 적절한지 재확인하는 게 안전합니다.

아래 스크립트로 보안 설정 위치를 빠르게 조회해 보세요.


🏁 Script executed:

#!/usr/bin/env bash
# Spring Security/CORS 설정 탐색
rg -n -C3 '(SecurityFilterChain|WebSecurityConfigurerAdapter|CorsConfiguration|allowedOrigins?|allowedOriginPatterns|setAllowCredentials|csrf\()' \
  -g '!**/build/**' -g '!**/out/**' -g '!**/target/**'

Length of output: 7034


조치 권고 — /reissue 엔드포인트의 CSRF/CORS 보호 재검증 필요

검증 요약: SecurityConfiguration에서 CSRF가 비활성화(.csrf(AbstractHttpConfigurer::disable))되어 있고 CORS는 corsProperties.allowedOrigins()를 사용해 구성하며 configuration.setAllowCredentials(true)가 설정되어 있습니다.
확인한 위치:

  • src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java — corsConfigurationSource()에서 setAllowedOrigins(corsProperties.allowedOrigins()) 및 configuration.setAllowCredentials(true) 설정, .csrf(...) 비활성화.
  • src/main/java/com/example/solidconnection/security/config/CorsProperties.java — allowedOrigins를 properties로 주입함.
  • src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java — registerStompEndpoints에서 setAllowedOrigins(allowedOrigins) 사용.
  • src/main/java/com/example/solidconnection/auth/controller/AuthController.java — /reissue 엔드포인트가 쿠키에서 리프레시 토큰을 읽음.

권장 변경(간단 워크스루):

  1. CORS 허용 출처 고정.
    • corsProperties.allowedOrigins에 정확한 신뢰 출처만 명시하고 "*" 또는 광범위 패턴을 사용하지 마세요.
  2. Credential 허용 시 정책 검증.
    • allowCredentials(true)를 유지한다면 허용 출처 목록만 사용하거나 요청 Origin을 반사하도록 구현해 불특정 출처에 자격증명이 노출되지 않게 하세요.
  3. CSRF 보호 적용 또는 대체 메커니즘 도입.
    • 쿠키 기반 리프레시를 유지하면 /reissue에 대해 CSRF를 활성화하거나 더블 서브밋 토큰 또는 별도 CSRF 헤더 검증을 구현하세요.
  4. 쿠키 속성 강화.
    • 리프레시 쿠키에 HttpOnly, Secure, SameSite=None(교차 출처 사용 시)을 설정하고 도메인/경로 제한을 최소 권한으로 적용하세요.
  5. WebSocket 엔드포인트 검토.
    • STOMP 핸드셰이크의 setAllowedOrigins도 동일 원칙으로 제한하고 핸드셰이크 인터셉터에서 추가 검증을 고려하세요.

한 줄 결론: 전역 CSRF 비활성화 + credentials 허용 조합은 리프레시 토큰을 쿠키로 읽는 상황에서 위험하므로 위 권장사항 중 최소 하나 이상을 적용해 주세요.

🤖 Prompt for AI Agents
In src/main/java/com/example/solidconnection/auth/controller/AuthController.java
around lines 119 to 125, the /reissue endpoint reads a refresh token from a
cookie while the app globally disables CSRF and allows credentials via CORS—this
combination is unsafe; update the security configuration and cookie handling as
follows: restrict corsProperties.allowedOrigins to explicit trusted origins
(remove "*" and wide patterns) or implement origin-reflection that only echoes
validated origins when configuration.setAllowCredentials(true) is used; enable
CSRF protection for cookie-based refresh or implement a CSRF mitigation such as
double-submit cookie or a required custom CSRF header for the /reissue endpoint;
harden the refresh cookie by setting HttpOnly, Secure, SameSite (None only if
cross-origin needed), and appropriate domain/path scope; and apply the same
origin restrictions and optional handshake validation to STOMP/WebSocket
endpoints.

}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.example.solidconnection.auth.controller;

import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_NOT_EXISTS;

import com.example.solidconnection.auth.controller.config.RefreshTokenCookieProperties;
import com.example.solidconnection.auth.domain.TokenType;
import com.example.solidconnection.common.exception.CustomException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
Expand Down Expand Up @@ -44,4 +50,26 @@ private void setRefreshTokenCookie(
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}

public String getRefreshToken(HttpServletRequest request) {
// 쿠키가 없거나 비어있는 경우 예외 발생
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length == 0) {
throw new CustomException(REFRESH_TOKEN_NOT_EXISTS);
}

// refreshToken 쿠키가 없는 경우 예외 발생
Cookie refreshTokenCookie = Arrays.stream(cookies)
.filter(cookie -> COOKIE_NAME.equals(cookie.getName()))
.findFirst()
.orElseThrow(() -> new CustomException(REFRESH_TOKEN_NOT_EXISTS));

// 쿠키 값이 비어있는 경우 예외 발생
String refreshToken = refreshTokenCookie.getValue();
if (refreshToken == null || refreshToken.isBlank()) {
throw new CustomException(REFRESH_TOKEN_NOT_EXISTS);
}
return refreshToken;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_EXPIRED;
import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;

import com.example.solidconnection.auth.dto.ReissueRequest;
import com.example.solidconnection.auth.dto.ReissueResponse;
import com.example.solidconnection.auth.token.TokenBlackListService;
import com.example.solidconnection.common.exception.CustomException;
Expand All @@ -28,12 +27,8 @@ public class AuthService {
* - 리프레시 토큰을 삭제한다.
* */
public void signOut(String token) {
Subject subject = authTokenProvider.parseSubject(token);
long siteUserId = Long.parseLong(subject.value());
SiteUser siteUser = siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));

AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
SiteUser siteUser = authTokenProvider.parseSiteUser(token);
AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser);
authTokenProvider.deleteRefreshTokenByAccessToken(accessToken);
tokenBlackListService.addToBlacklist(accessToken);
}
Comment on lines 29 to 34
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

1) signOut에서 새 토큰을 생성해 블랙리스트에 올리는 버그가 있습니다.

현재 코드는 신규 AccessToken을 만들어 블랙리스트에 올려 기존 토큰이 유효한 상태로 남습니다. 전달받은 실제 토큰 문자열을 블랙리스트에 올려야 합니다.

아래와 같이 원본 토큰 문자열로 AccessToken 객체를 구성해 사용하도록 수정해 주세요.

 public void signOut(String token) {
-    SiteUser siteUser = authTokenProvider.parseSiteUser(token);
-    AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser);
+    SiteUser siteUser = authTokenProvider.parseSiteUser(token);
+    AccessToken accessToken = new AccessToken(
+            authTokenProvider.toSubject(siteUser),
+            siteUser.getRole(),
+            token
+    );
     authTokenProvider.deleteRefreshTokenByAccessToken(accessToken);
     tokenBlackListService.addToBlacklist(accessToken);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void signOut(String token) {
Subject subject = authTokenProvider.parseSubject(token);
long siteUserId = Long.parseLong(subject.value());
SiteUser siteUser = siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
SiteUser siteUser = authTokenProvider.parseSiteUser(token);
AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser);
authTokenProvider.deleteRefreshTokenByAccessToken(accessToken);
tokenBlackListService.addToBlacklist(accessToken);
}
public void signOut(String token) {
SiteUser siteUser = authTokenProvider.parseSiteUser(token);
AccessToken accessToken = new AccessToken(
authTokenProvider.toSubject(siteUser),
siteUser.getRole(),
token
);
authTokenProvider.deleteRefreshTokenByAccessToken(accessToken);
tokenBlackListService.addToBlacklist(accessToken);
}
🤖 Prompt for AI Agents
In src/main/java/com/example/solidconnection/auth/service/AuthService.java
around lines 29 to 34, the signOut method wrongly generates a new AccessToken
and blacklists that new token instead of the original token string; remove the
call that generates a new token, construct or wrap an AccessToken using the
original token string passed into signOut, and pass that AccessToken instance
(built from the original token) to
authTokenProvider.deleteRefreshTokenByAccessToken(...) and
tokenBlackListService.addToBlacklist(...).

Expand All @@ -58,17 +53,14 @@ public void quit(long siteUserId, String token) {
* - 유효한 리프레시토큰이면, 액세스 토큰을 재발급한다.
* - 그렇지 않으면 예외를 발생시킨다.
* */
public ReissueResponse reissue(long siteUserId, ReissueRequest reissueRequest) {
public ReissueResponse reissue(String requestedRefreshToken) {
// 리프레시 토큰 확인
String requestedRefreshToken = reissueRequest.refreshToken();
if (!authTokenProvider.isValidRefreshToken(requestedRefreshToken)) {
throw new CustomException(REFRESH_TOKEN_EXPIRED);
}
// 액세스 토큰 재발급
SiteUser siteUser = siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
Subject subject = authTokenProvider.parseSubject(requestedRefreshToken);
AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
SiteUser siteUser = authTokenProvider.parseSiteUser(requestedRefreshToken);
AccessToken newAccessToken = authTokenProvider.generateAccessToken(siteUser);
Copy link
Member

Choose a reason for hiding this comment

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

generateAccessToken 을 사용하는 주체 입장에서는 간단하고 좋네요 !

return ReissueResponse.from(newAccessToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.example.solidconnection.auth.service;

import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;

import com.example.solidconnection.auth.domain.TokenType;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
Expand All @@ -17,15 +21,21 @@ public class AuthTokenProvider {

private final RedisTemplate<String, String> redisTemplate;
private final TokenProvider tokenProvider;
private final SiteUserRepository siteUserRepository;

public AccessToken generateAccessToken(Subject subject, Role role) {
public AccessToken generateAccessToken(SiteUser siteUser) {
Subject subject = toSubject(siteUser);
Role role = siteUser.getRole();
String token = tokenProvider.generateToken(
subject.value(), Map.of(ROLE_CLAIM_KEY, role.name()), TokenType.ACCESS
subject.value(),
Map.of(ROLE_CLAIM_KEY, role.name()),
TokenType.ACCESS
);
return new AccessToken(subject, role, token);
}

public RefreshToken generateAndSaveRefreshToken(Subject subject) {
public RefreshToken generateAndSaveRefreshToken(SiteUser siteUser) {
Subject subject = toSubject(siteUser);
String token = tokenProvider.generateToken(subject.value(), TokenType.REFRESH);
tokenProvider.saveToken(token, TokenType.REFRESH);
return new RefreshToken(subject, token);
Expand All @@ -49,9 +59,11 @@ public void deleteRefreshTokenByAccessToken(AccessToken accessToken) {
redisTemplate.delete(refreshTokenKey);
}

public Subject parseSubject(String token) {
public SiteUser parseSiteUser(String token) {
String subject = tokenProvider.parseSubject(token);
return new Subject(subject);
long siteUserId = Long.parseLong(subject);
return siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
}
Comment on lines +62 to 67
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

1) parseSiteUser에서 NumberFormatException을 처리해 500을 방지해 주세요.

비정상 토큰의 subject가 숫자가 아니면 런타임 예외로 500이 날 수 있습니다. 커스텀 예외로 변환해 4xx로 응답하는 게 안전합니다.

다음과 같이 보강을 제안합니다.

 public SiteUser parseSiteUser(String token) {
     String subject = tokenProvider.parseSubject(token);
-    long siteUserId = Long.parseLong(subject);
+    long siteUserId;
+    try {
+        siteUserId = Long.parseLong(subject);
+    } catch (NumberFormatException e) {
+        // 토큰 변조/형식 오류를 서버 에러로 노출하지 않도록 처리
+        throw new CustomException(USER_NOT_FOUND);
+    }
     return siteUserRepository.findById(siteUserId)
             .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
 }

원하시면, 이 경로에 대한 단위 테스트(비숫자 subject 케이스) 추가도 도와드리겠습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public SiteUser parseSiteUser(String token) {
String subject = tokenProvider.parseSubject(token);
return new Subject(subject);
long siteUserId = Long.parseLong(subject);
return siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
}
public SiteUser parseSiteUser(String token) {
String subject = tokenProvider.parseSubject(token);
long siteUserId;
try {
siteUserId = Long.parseLong(subject);
} catch (NumberFormatException e) {
// 토큰 변조/형식 오류를 서버 에러로 노출하지 않도록 처리
throw new CustomException(USER_NOT_FOUND);
}
return siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
}
🤖 Prompt for AI Agents
In src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java
around lines 62-67, parseSiteUser currently calls Long.parseLong(subject)
directly which can throw NumberFormatException for non-numeric subjects and
cause a 500; wrap the parse in a try-catch, catch NumberFormatException and
throw an appropriate CustomException (e.g., INVALID_TOKEN or a new
INVALID_SUBJECT) so the error maps to a 4xx response, keeping the repository
lookup and existing USER_NOT_FOUND behavior unchanged.


public Subject toSubject(SiteUser siteUser) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ public class SignInService {
@Transactional
public SignInResponse signIn(SiteUser siteUser) {
resetQuitedAt(siteUser);
Subject subject = authTokenProvider.toSubject(siteUser);
AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject);
AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser);
RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser);
return SignInResponse.of(accessToken, refreshToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public enum ErrorCode {
ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."),
REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."),
ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다."),
REFRESH_TOKEN_NOT_EXISTS(HttpStatus.BAD_REQUEST.value(), "리프레시 토큰이 존재하지 않습니다."),
PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST.value(), "비밀번호가 일치하지 않습니다."),
PASSWORD_NOT_CHANGED(HttpStatus.BAD_REQUEST.value(), "현재 비밀번호와 새 비밀번호가 동일합니다."),
PASSWORD_NOT_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "새 비밀번호가 일치하지 않습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
package com.example.solidconnection.auth.controller;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.BDDMockito.given;

import com.example.solidconnection.auth.controller.config.RefreshTokenCookieProperties;
import com.example.solidconnection.auth.domain.TokenType;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.common.exception.ErrorCode;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

@DisplayName("리프레시 토큰 쿠키 매니저 테스트")
@TestContainerSpringBootTest
class RefreshTokenCookieManagerTest {

private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken";

@Autowired
private RefreshTokenCookieManager cookieManager;

Expand Down Expand Up @@ -46,7 +56,7 @@ void setUp() {
String header = response.getHeader("Set-Cookie");
assertAll(
() -> assertThat(header).isNotNull(),
() -> assertThat(header).contains("refreshToken=" + refreshToken),
() -> assertThat(header).contains(REFRESH_TOKEN_COOKIE_NAME + "=" + refreshToken),
() -> assertThat(header).contains("HttpOnly"),
() -> assertThat(header).contains("Secure"),
() -> assertThat(header).contains("Path=/"),
Expand All @@ -68,14 +78,67 @@ void setUp() {
String header = response.getHeader("Set-Cookie");
assertAll(
() -> assertThat(header).isNotNull(),
() -> assertThat(header).contains("refreshToken="),
() -> assertThat(header).contains(REFRESH_TOKEN_COOKIE_NAME + "="),
() -> assertThat(header).contains("HttpOnly"),
() -> assertThat(header).contains("Secure"),
() -> assertThat(header).contains("Path=/"),
() -> assertThat(header).contains("Max-Age=0"),
() -> assertThat(header).contains("SameSite=Strict"),
() -> assertThat(header).contains("Domain=" + domain),
() -> assertThat(header).contains("SameSite=" + sameSite)
);
}

@Nested
class 쿠키에서_리프레시_토큰을_추출한다 {

@Test
void 리프레시_토큰이_있으면_정상_반환한다() {
// given
MockHttpServletRequest request = new MockHttpServletRequest();
String refreshToken = "test-refresh-token";
request.setCookies(new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken));

// when
String retrievedToken = cookieManager.getRefreshToken(request);

// then
assertThat(retrievedToken).isEqualTo(refreshToken);
}

@Test
void 쿠키가_없으면_예외가_발생한다() {
// given
MockHttpServletRequest request = new MockHttpServletRequest();

// when & then
assertThatCode(() -> cookieManager.getRefreshToken(request))
.isInstanceOf(CustomException.class)
.hasMessageContaining(ErrorCode.REFRESH_TOKEN_NOT_EXISTS.getMessage());
}

@Test
void 리프레시_토큰_쿠키가_없으면_예외가_발생한다() {
// given
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(new Cookie("otherCookie", "some-value"));

// when & then
assertThatCode(() -> cookieManager.getRefreshToken(request))
.isInstanceOf(CustomException.class)
.hasMessageContaining(ErrorCode.REFRESH_TOKEN_NOT_EXISTS.getMessage());
}

@ParameterizedTest
@ValueSource(strings = {"", " "})
void 리프레시_토큰_쿠키가_비어있으면_예외가_발생한다(String token) {
// given
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(new Cookie(REFRESH_TOKEN_COOKIE_NAME, token));

// when & then
assertThatCode(() -> cookieManager.getRefreshToken(request))
.isInstanceOf(CustomException.class)
.hasMessageContaining(ErrorCode.REFRESH_TOKEN_NOT_EXISTS.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import static org.junit.jupiter.api.Assertions.assertAll;

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.auth.token.TokenBlackListService;
import com.example.solidconnection.common.exception.CustomException;
Expand Down Expand Up @@ -45,14 +44,12 @@ class AuthServiceTest {
private SiteUserRepository siteUserRepository;

private SiteUser siteUser;
private Subject subject;
private AccessToken accessToken;

@BeforeEach
void setUp() {
siteUser = siteUserFixture.사용자();
subject = authTokenProvider.toSubject(siteUser);
accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
accessToken = authTokenProvider.generateAccessToken(siteUser);
}

@Test
Expand All @@ -61,7 +58,7 @@ void setUp() {
authService.signOut(accessToken.token());

// then
String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value());
String refreshTokenKey = TokenType.REFRESH.addPrefix(accessToken.subject().value());
assertAll(
() -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(),
() -> assertThat(tokenBlackListService.isTokenBlacklisted(accessToken.token())).isTrue()
Expand All @@ -75,7 +72,7 @@ void setUp() {

// then
LocalDate tomorrow = LocalDate.now().plusDays(1);
String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value());
String refreshTokenKey = TokenType.REFRESH.addPrefix(accessToken.subject().value());
SiteUser actualSitUser = siteUserRepository.findById(siteUser.getId()).orElseThrow();
assertAll(
() -> assertThat(actualSitUser.getQuitedAt()).isEqualTo(tomorrow),
Expand All @@ -90,26 +87,24 @@ class 토큰을_재발급한다 {
@Test
void 요청의_리프레시_토큰이_저장되어_있으면_액세스_토큰을_재발급한다() {
// given
RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(new Subject("subject"));
ReissueRequest reissueRequest = new ReissueRequest(refreshToken.token());
RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser);

// when
ReissueResponse reissuedAccessToken = authService.reissue(siteUser.getId(), reissueRequest);
ReissueResponse reissuedAccessToken = authService.reissue(refreshToken.token());

// then - 요청의 리프레시 토큰과 재발급한 액세스 토큰의 subject 가 동일해야 한다.
Subject expectedSubject = authTokenProvider.parseSubject(refreshToken.token());
Subject actualSubject = authTokenProvider.parseSubject(reissuedAccessToken.accessToken());
assertThat(actualSubject).isEqualTo(expectedSubject);
// then - 요청의 리프레시 토큰과 재발급한 액세스 토큰의 주체가 동일해야 한다.
SiteUser actualSiteUser = authTokenProvider.parseSiteUser(refreshToken.token());
SiteUser expectedSiteUser = authTokenProvider.parseSiteUser(reissuedAccessToken.accessToken());
assertThat(actualSiteUser.getId()).isEqualTo(expectedSiteUser.getId());
}

@Test
void 요청의_리프레시_토큰이_저장되어있지_않다면_예외가_발생한다() {
// given
String invalidRefreshToken = accessToken.token();
ReissueRequest reissueRequest = new ReissueRequest(invalidRefreshToken);

// when, then
assertThatCode(() -> authService.reissue(siteUser.getId(), reissueRequest))
assertThatCode(() -> authService.reissue(invalidRefreshToken))
.isInstanceOf(CustomException.class)
.hasMessage(REFRESH_TOKEN_EXPIRED.getMessage());
}
Expand Down
Loading
Loading