Skip to content

[Refactor] #59 Refresh Token 로테이션 적용#61

Merged
seokjun01 merged 3 commits intodevelopfrom
refactor/#59/JWT-토큰-관리-개선
Feb 24, 2026

Hidden character warning

The head ref may contain hidden characters: "refactor/#59/JWT-\ud1a0\ud070-\uad00\ub9ac-\uac1c\uc120"
Merged

[Refactor] #59 Refresh Token 로테이션 적용#61
seokjun01 merged 3 commits intodevelopfrom
refactor/#59/JWT-토큰-관리-개선

Conversation

@seokjun01
Copy link
Copy Markdown
Collaborator

@seokjun01 seokjun01 commented Feb 24, 2026

📌 작업한 내용

AccessToken의 만료로 인하여 재발급 요청 시 , RefreshToken의 탈취를 방지하기 위하여
같이 재발급 되게끔 로테이션을 적용하였습니다.

코드는 클로드코드가 작성하였으나 ,TDD기반으로
case 1)유효한 RefreshToken으로 요청하면 AccessToken과 RefreshToken이 모두 갱신된다
case 2)DB에 존재하지 않는 RefreshToken으로 요청하면 INVALID_REFRESH_TOKEN 예외가 발생한다
의 검증이 통과하는지를 확인하였습니다.

🔍 참고 사항

🖼️ 스크린샷

스크린샷 2026-02-24 오후 5 33 09

🔗 관련 이슈

✅ 체크리스트

  • 로컬에서 빌드 및 테스트 완료
  • 코드 리뷰 반영 완료
  • 문서화 필요 여부 확인

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced token refresh mechanism now issues both new access and refresh tokens simultaneously, replacing the previous single-token approach.
  • Tests

    • Added comprehensive test coverage for token refresh functionality, including validation of successful refresh, invalid token handling, and expired token scenarios.

@seokjun01 seokjun01 requested a review from jinisim February 24, 2026 08:41
@seokjun01 seokjun01 self-assigned this Feb 24, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

The PR modifies the authentication token refresh mechanism to rotate both access and refresh tokens instead of just the access token. A new test fixture is added, and comprehensive test coverage is introduced for the updated token refresh functionality.

Changes

Cohort / File(s) Summary
Token Refresh Implementation
src/main/java/tave/crezipsa/.../auth/usecase/AuthUsecaseImpl.java
Updated reissueAccessToken method to generate and update both new access and refresh tokens via updateTokens(), implementing two-token rotation instead of single-token refresh.
Token Refresh Tests
src/test/java/tave/crezipsa/.../auth/usecase/AuthUsecaseImplTest.java
Added three new test cases validating token refresh behavior: successful dual-token rotation, invalid refresh token rejection, and expired refresh token handling with appropriate error responses.
Test Fixtures
src/test/java/tave/crezipsa/.../fixture/AuthFixture.java
Introduced new AuthFixture utility class providing factory methods createAuth() to construct Auth domain entities with configurable tokens for testing purposes.

Sequence Diagram

sequenceDiagram
    participant Test as Test/Client
    participant AuthUsecase as AuthUsecase
    participant TokenGen as Token Generator
    participant AuthEntity as Auth Entity
    participant AuthRepo as Auth Repository

    Test->>AuthUsecase: reissueAccessToken(refreshToken)
    AuthUsecase->>AuthUsecase: validateRefreshToken()
    alt Token Valid
        AuthUsecase->>TokenGen: generateAccessToken()
        TokenGen-->>AuthUsecase: newAccessToken
        AuthUsecase->>TokenGen: generateRefreshToken()
        TokenGen-->>AuthUsecase: newRefreshToken
        AuthUsecase->>AuthEntity: updateTokens(newAccessToken, newRefreshToken)
        AuthEntity-->>AuthUsecase: updated
        AuthUsecase->>AuthRepo: save(authEntity)
        AuthRepo-->>AuthUsecase: saved
        AuthUsecase-->>Test: TokenResponse
    else Token Invalid/Expired
        AuthUsecase-->>Test: CommonException/JwtAuthenticationException
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • jinisim

Poem

🐰 Two tokens dance in the refresh ballet,
Old ones fade as new ones come to play,
Rotation spins with validation's care,
Security wrapped in fixtures fair! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: implementing refresh token rotation for JWT token management improvement, which aligns with the core modification of the PR that changes the token refresh flow to rotate both access and refresh tokens.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#59/JWT-토큰-관리-개선

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@seokjun01 seokjun01 merged commit af282d1 into develop Feb 24, 2026
2 of 3 checks passed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/main/java/tave/crezipsa/crezipsa/application/auth/usecase/AuthUsecaseImpl.java (2)

35-37: Rotation is implemented correctly; consider adding refresh token reuse detection.

The core rotation logic is sound — both tokens are regenerated and persisted atomically inside the existing @Transactional boundary. No issues with the change itself.

One security gap worth tracking as a follow-up: the current implementation provides no token-reuse detection. The typical attack scenario is: a refresh token is stolen, the attacker exchanges it for a new one before the legitimate user does, the server issues a new refresh token to the attacker and invalidates the old one — the legitimate user is then locked out. Without reuse detection, this scenario enables an attacker to maintain persistence while the degraded UX is attributed to an application bug rather than active compromise.

Automatic reuse detection works by having the server recognize that a previously-used (now-superseded) refresh token is being presented; since it cannot determine whether the legitimate user or an attacker is holding the current token, it invalidates the entire token family just to be safe. This would mean: on receiving a refresh request with a token that is cryptographically valid but no longer in the DB (i.e., already rotated), all sessions for that userId should be revoked rather than just returning INVALID_REFRESH_TOKEN.

This is a meaningful enhancement that can be a follow-up issue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/auth/usecase/AuthUsecaseImpl.java`
around lines 35 - 37, In AuthUsecaseImpl where refresh tokens are rotated
(calling tokenProvider.generateAccessToken / generateRefreshToken and
auth.updateTokens), add refresh-token-reuse detection: when a refresh request
presents a cryptographically valid token that does not match the current stored
refresh token for the auth record, treat that as reuse and revoke the entire
token family for that userId (e.g., call a new method on the auth
repository/service such as revokeAllSessions(userId) or
invalidateAllTokens(userId)) instead of merely returning INVALID_REFRESH_TOKEN;
implement the lookup/compare before generating new tokens and ensure the
revoke-all operation is atomic/transactional alongside existing token updates.

35-37: Remove the dead code method Auth.updateAccessToken().

The updateAccessToken() method is no longer called anywhere in the codebase after the transition to updateTokens(). This method can be safely removed from the Auth entity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/auth/usecase/AuthUsecaseImpl.java`
around lines 35 - 37, Remove the unused/dead method Auth.updateAccessToken()
from the Auth entity: locate the Auth class and delete the
updateAccessToken(...) method signature and body, ensure no remaining references
(the code now uses updateTokens(...) as in AuthUsecaseImpl where
newAccessToken/newRefreshToken are set), run a project-wide search for
updateAccessToken to confirm it’s not used and update any tests or interfaces
that referenced it (replace calls with updateTokens if found).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/test/java/tave/crezipsa/crezipsa/application/auth/usecase/AuthUsecaseImplTest.java`:
- Around line 51-72: Add an assertion for the userId in the
reissueAccessToken_success_rotatesBothTokens test: after invoking
authUsecase.reissueAccessToken(request) assert that response.userId() equals the
expected userId (the local variable userId = 1L) to ensure TokenResponse
includes the correct user identifier; update the test method
reissueAccessToken_success_rotatesBothTokens to include this check alongside the
existing accessToken and refreshToken assertions.

---

Nitpick comments:
In
`@src/main/java/tave/crezipsa/crezipsa/application/auth/usecase/AuthUsecaseImpl.java`:
- Around line 35-37: In AuthUsecaseImpl where refresh tokens are rotated
(calling tokenProvider.generateAccessToken / generateRefreshToken and
auth.updateTokens), add refresh-token-reuse detection: when a refresh request
presents a cryptographically valid token that does not match the current stored
refresh token for the auth record, treat that as reuse and revoke the entire
token family for that userId (e.g., call a new method on the auth
repository/service such as revokeAllSessions(userId) or
invalidateAllTokens(userId)) instead of merely returning INVALID_REFRESH_TOKEN;
implement the lookup/compare before generating new tokens and ensure the
revoke-all operation is atomic/transactional alongside existing token updates.
- Around line 35-37: Remove the unused/dead method Auth.updateAccessToken() from
the Auth entity: locate the Auth class and delete the updateAccessToken(...)
method signature and body, ensure no remaining references (the code now uses
updateTokens(...) as in AuthUsecaseImpl where newAccessToken/newRefreshToken are
set), run a project-wide search for updateAccessToken to confirm it’s not used
and update any tests or interfaces that referenced it (replace calls with
updateTokens if found).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 774d206 and 59a4431.

📒 Files selected for processing (3)
  • src/main/java/tave/crezipsa/crezipsa/application/auth/usecase/AuthUsecaseImpl.java
  • src/test/java/tave/crezipsa/crezipsa/application/auth/usecase/AuthUsecaseImplTest.java
  • src/test/java/tave/crezipsa/crezipsa/fixture/AuthFixture.java

Comment on lines +51 to +72
void reissueAccessToken_success_rotatesBothTokens() {
// Given
Long userId = 1L;
String oldRefreshToken = "old-refresh-token";
String newAccessToken = "new-access-token";
String newRefreshToken = "new-refresh-token";

Auth auth = createAuth(userId, "old-access-token", oldRefreshToken);
RefreshTokenRequest request = new RefreshTokenRequest(userId, oldRefreshToken);

when(authRepository.findByUserIdAndRefreshToken(userId, oldRefreshToken))
.thenReturn(Optional.of(auth));
when(tokenProvider.generateAccessToken(userId)).thenReturn(newAccessToken);
when(tokenProvider.generateRefreshToken(userId)).thenReturn(newRefreshToken);

// When
TokenResponse response = authUsecase.reissueAccessToken(request);

// Then
assertThat(response.accessToken()).isEqualTo(newAccessToken);
assertThat(response.refreshToken()).isEqualTo(newRefreshToken);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing userId assertion in the rotation success test.

The test verifies that both accessToken and refreshToken are rotated correctly, but does not assert the userId field on the TokenResponse. Given that TokenResponse.from(auth) populates userId from the entity, and userId is included in the API response (visible in the PR screenshot), the test's coverage of the response shape is incomplete.

✅ Proposed addition
         // Then
+        assertThat(response.userId()).isEqualTo(userId);
         assertThat(response.accessToken()).isEqualTo(newAccessToken);
         assertThat(response.refreshToken()).isEqualTo(newRefreshToken);
📝 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
void reissueAccessToken_success_rotatesBothTokens() {
// Given
Long userId = 1L;
String oldRefreshToken = "old-refresh-token";
String newAccessToken = "new-access-token";
String newRefreshToken = "new-refresh-token";
Auth auth = createAuth(userId, "old-access-token", oldRefreshToken);
RefreshTokenRequest request = new RefreshTokenRequest(userId, oldRefreshToken);
when(authRepository.findByUserIdAndRefreshToken(userId, oldRefreshToken))
.thenReturn(Optional.of(auth));
when(tokenProvider.generateAccessToken(userId)).thenReturn(newAccessToken);
when(tokenProvider.generateRefreshToken(userId)).thenReturn(newRefreshToken);
// When
TokenResponse response = authUsecase.reissueAccessToken(request);
// Then
assertThat(response.accessToken()).isEqualTo(newAccessToken);
assertThat(response.refreshToken()).isEqualTo(newRefreshToken);
}
void reissueAccessToken_success_rotatesBothTokens() {
// Given
Long userId = 1L;
String oldRefreshToken = "old-refresh-token";
String newAccessToken = "new-access-token";
String newRefreshToken = "new-refresh-token";
Auth auth = createAuth(userId, "old-access-token", oldRefreshToken);
RefreshTokenRequest request = new RefreshTokenRequest(userId, oldRefreshToken);
when(authRepository.findByUserIdAndRefreshToken(userId, oldRefreshToken))
.thenReturn(Optional.of(auth));
when(tokenProvider.generateAccessToken(userId)).thenReturn(newAccessToken);
when(tokenProvider.generateRefreshToken(userId)).thenReturn(newRefreshToken);
// When
TokenResponse response = authUsecase.reissueAccessToken(request);
// Then
assertThat(response.userId()).isEqualTo(userId);
assertThat(response.accessToken()).isEqualTo(newAccessToken);
assertThat(response.refreshToken()).isEqualTo(newRefreshToken);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/tave/crezipsa/crezipsa/application/auth/usecase/AuthUsecaseImplTest.java`
around lines 51 - 72, Add an assertion for the userId in the
reissueAccessToken_success_rotatesBothTokens test: after invoking
authUsecase.reissueAccessToken(request) assert that response.userId() equals the
expected userId (the local variable userId = 1L) to ensure TokenResponse
includes the correct user identifier; update the test method
reissueAccessToken_success_rotatesBothTokens to include this check alongside the
existing accessToken and refreshToken assertions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant