Skip to content

FEAT: 리프레시 토큰 방식 추가 & api 엔드포인트 변경#77

Merged
icarus0616 merged 1 commit into
devfrom
feat/2/login
Jan 28, 2026
Merged

FEAT: 리프레시 토큰 방식 추가 & api 엔드포인트 변경#77
icarus0616 merged 1 commit into
devfrom
feat/2/login

Conversation

@jaehyeon4406
Copy link
Copy Markdown
Collaborator

@jaehyeon4406 jaehyeon4406 commented Jan 28, 2026

Summary by CodeRabbit

  • New Features

    • Added token refresh functionality enabling users to obtain new access tokens without re-authentication.
    • New authentication endpoint for token refresh available.
    • Login responses now include refresh tokens with 14-day expiration.
  • Documentation

    • Enhanced API documentation with Swagger/OpenAPI metadata for authentication endpoints and responses.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

This PR introduces refresh token functionality to the authentication system. It adds a new RefreshToken entity with repository, extends JWT utilities to generate refresh tokens, implements a token refresh endpoint with validation and rotation, updates DTOs to include refresh tokens, and adds configuration for refresh token expiration alongside security updates.

Changes

Cohort / File(s) Summary
Refresh Token Entity & Repository
RefreshToken.java, RefreshTokenRepository.java
New JPA entity RefreshToken with id, token, email, expiryDate fields; provides isExpired() and updateToken() methods. Companion repository interface exposes findByToken(), findByEmail(), and deleteByEmail() query methods.
JWT & Token Configuration
JwtUtil.java, application.yaml
Added createRefreshToken() method, getRefreshExpiration(), and getAccessExpirationMillis() to JwtUtil; constructor now accepts refresh expiration parameter. Configuration adds jwt.token.expiration.refresh: 1209600000 (14 days).
DTO Updates & Swagger Annotations
AuthReqDTO.java, AuthResDTO.java
AuthReqDTO adds @Schema annotations to LoginDTO, JoinDTO, RefreshTokenDTO. AuthResDTO adds refreshToken field to LoginDTO and @Schema annotations for both JoinDTO and LoginDTO; replaced NotBlank import with Schema import.
Authentication Service Layer
AuthQueryService.java, AuthQueryServiceImpl.java
Added refreshToken() method to interface. Implementation injects RefreshTokenRepository, adds refresh token lifecycle management to login(), and implements new refreshToken() with @Transactional annotation handling validation, token rotation, and database persistence.
Controller & Converter
AuthController.java, AuthConverter.java
AuthController adds POST /api/v1/auth/refresh endpoint with Swagger/OpenAPI metadata. AuthConverter updates toLoginDTO() signature from (AuthAccounts, String) to (AuthAccounts, String, String, Long), now populating refreshToken and accepting dynamic expiresIn instead of hardcoded value.
Security & API Response Documentation
SecurityConfig.java, ApiResponse.java
SecurityConfig consolidates auth URI allowlist from explicit "/sign-up" and "/login" to pattern "/api/v1/auth/**". ApiResponse adds class-level @Schema and field-level @Schema annotations for Swagger documentation without altering control flow.

Sequence Diagram

sequenceDiagram
    participant Client
    participant AuthController
    participant AuthQueryService
    participant JwtUtil
    participant RefreshTokenRepository
    participant Database
    
    Client->>AuthController: POST /api/v1/auth/refresh<br/>(refreshTokenDTO)
    AuthController->>AuthQueryService: refreshToken(dto)
    AuthQueryService->>RefreshTokenRepository: findByToken(token)
    RefreshTokenRepository->>Database: Query refresh_tokens
    Database-->>RefreshTokenRepository: RefreshToken record
    RefreshTokenRepository-->>AuthQueryService: Optional<RefreshToken>
    
    alt Token valid & not expired
        AuthQueryService->>AuthQueryService: Load account by email
        AuthQueryService->>JwtUtil: createAccessToken(userDetails)
        JwtUtil-->>AuthQueryService: New access token
        AuthQueryService->>JwtUtil: createRefreshToken(userDetails)
        JwtUtil-->>AuthQueryService: New refresh token
        AuthQueryService->>RefreshTokenRepository: Save updated token
        RefreshTokenRepository->>Database: Persist new refresh token
        AuthQueryService-->>AuthController: LoginDTO (access+refresh)
    else Token invalid or expired
        AuthQueryService-->>AuthController: Exception (NOT_FOUND/INVALID)
    end
    
    AuthController-->>Client: ApiResponse<LoginDTO>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 With tokens that bloom and renew,
No more expiration blues!
Refresh and rotate with grace,
Authentication finds its place! 🔐✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.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 accurately describes the main changes: introducing refresh token support and modifying authentication API endpoints as evidenced by new RefreshToken entity, RefreshTokenRepository, refreshToken endpoint, and SecurityConfig URI changes.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@icarus0616 icarus0616 self-requested a review January 28, 2026 13:05
@icarus0616 icarus0616 self-assigned this Jan 28, 2026
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: 5

🤖 Fix all issues with AI agents
In `@booklog/src/main/java/com/example/booklog/global/auth/dto/AuthReqDTO.java`:
- Around line 29-30: The `@Schema` annotation on the RefreshTokenDTO record uses a
misspelled name "AuthRefreshToeknRequest"; update the annotation value to
"AuthRefreshTokenRequest" so Swagger docs show the correct name — locate the
`@Schema`(...) on the RefreshTokenDTO record and correct the schema string.

In
`@booklog/src/main/java/com/example/booklog/global/auth/entity/RefreshToken.java`:
- Around line 22-23: The RefreshToken entity currently stores the raw token
string in the token field; change this to store a secure hash instead and update
creation/validation logic accordingly: replace or rename the token field (e.g.,
hashedToken) and persist a one-way hash of the generated token (use a strong
algorithm like HMAC-SHA256 with a per-token random salt or bcrypt/Argon2) rather
than the plaintext, ensure uniqueness is enforced on the hash if desired, and
update the code paths that generate and validate refresh tokens to (1) return
the raw token to the client but only persist its hash in RefreshToken, and (2)
on refresh requests hash the incoming token the same way and compare hashes to
authenticate; also ensure salts/pepper and secure random generation of the
original token are handled in the token creation routine and that any repository
queries (e.g., findByToken) are updated to query by the hashed value.

In
`@booklog/src/main/java/com/example/booklog/global/auth/Repository/RefreshTokenRepository.java`:
- Line 1: The package declaration in RefreshTokenRepository.java uses a
capitalized segment ("Repository") which breaks Java conventions; change the
package line from com.example.booklog.global.auth.Repository to
com.example.booklog.global.auth.repository, move the file into the matching
directory (repository) and update any import statements or references across the
codebase that point to com.example.booklog.global.auth.Repository (including
usages of the RefreshTokenRepository type) so they use
com.example.booklog.global.auth.repository instead.

In `@booklog/src/main/java/com/example/booklog/global/auth/security/JwtUtil.java`:
- Around line 41-44: The refresh and access tokens are indistinguishable; update
the token creation flow so createRefreshToken (which calls createToken) adds a
claim like "type":"refresh" (and ensure access tokens include "type":"access");
then update JwtAuthFilter and the refreshToken handling logic to validate that
incoming tokens have the expected "type" claim (e.g., JwtAuthFilter only accepts
"access" and the refresh endpoint accepts "refresh") and reject tokens with the
wrong type. Ensure the claim key is consistent across token issuance and
validation and include tests for both createRefreshToken/createToken,
JwtAuthFilter, and the refreshToken flow.

In
`@booklog/src/main/java/com/example/booklog/global/auth/service/AuthQueryServiceImpl.java`:
- Around line 103-105: The current refresh flow in AuthQueryServiceImpl updates
storedToken via storedToken.updateToken(...) and
refreshTokenRepository.save(storedToken) which can race under concurrent
requests; add optimistic locking to the RefreshToken entity by adding a `@Version`
Long version field (and optionally ensure a unique constraint on the token
column), then wrap the save/update in AuthQueryServiceImpl with handling for
OptimisticLockException (catch it and translate to a controlled error/retry
response) so concurrent refresh attempts fail fast and are handled cleanly.
🧹 Nitpick comments (4)
booklog/src/main/java/com/example/booklog/global/auth/entity/RefreshToken.java (1)

32-33: Consider injecting a time source for expiry checks.

LocalDateTime.now() ties expiry to the server’s default timezone and complicates testing. Using a Clock (or Instant/OffsetDateTime) improves consistency and testability.

booklog/src/main/java/com/example/booklog/global/auth/controller/AuthController.java (1)

54-64: Consider adding a dedicated success code for token refresh.

Using AuthSuccessCode.LOGIN_SUCCESS for the refresh endpoint works but is semantically imprecise. Consider adding a TOKEN_REFRESH_SUCCESS code for clearer API responses.

booklog/src/main/resources/application.yaml (1)

54-56: Consider using environment variables for token expiration values.

The expiration values are reasonable (2 hours for access, 14 days for refresh). For production flexibility, consider using environment variables with defaults:

expiration:
  access: ${JWT_ACCESS_EXPIRATION:7200000} # 2시간
  refresh: ${JWT_REFRESH_EXPIRATION:1209600000} # 14일

This allows tuning without code changes.

booklog/src/main/java/com/example/booklog/global/auth/service/AuthQueryServiceImpl.java (1)

70-72: Consider moving refreshToken to AuthCommandService.

The refreshToken method performs mutations (token rotation, DB updates) which violates the query/command separation implied by AuthQueryService. While functionally correct, moving this to AuthCommandService would better reflect its behavior.

Comment on lines +29 to +30
@Schema(name = "AuthRefreshToeknRequest")
public record RefreshTokenDTO(
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

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "AuthReqDTO.java" | head -20

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 150


🏁 Script executed:

cat -n booklog/src/main/java/com/example/booklog/global/auth/dto/AuthReqDTO.java

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 1062


🏁 Script executed:

rg "Toekn|TokenRequest" --type java booklog/

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 194


Fix typo in schema name.

AuthRefreshToeknRequest is misspelled and will appear in Swagger documentation. Update to AuthRefreshTokenRequest.

Fix
-    `@Schema`(name = "AuthRefreshToeknRequest")
+    `@Schema`(name = "AuthRefreshTokenRequest")
🤖 Prompt for AI Agents
In `@booklog/src/main/java/com/example/booklog/global/auth/dto/AuthReqDTO.java`
around lines 29 - 30, The `@Schema` annotation on the RefreshTokenDTO record uses
a misspelled name "AuthRefreshToeknRequest"; update the annotation value to
"AuthRefreshTokenRequest" so Swagger docs show the correct name — locate the
`@Schema`(...) on the RefreshTokenDTO record and correct the schema string.

Comment on lines +22 to +23
@Column(nullable = false, unique = true)
private String token;
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 | 🟠 Major

Store refresh tokens hashed, not in plaintext.

Keeping the raw token in the DB means a data leak becomes an immediate token compromise. Prefer storing a hash (or a token identifier/JTI) and compare hashed inputs during refresh.

🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/global/auth/entity/RefreshToken.java`
around lines 22 - 23, The RefreshToken entity currently stores the raw token
string in the token field; change this to store a secure hash instead and update
creation/validation logic accordingly: replace or rename the token field (e.g.,
hashedToken) and persist a one-way hash of the generated token (use a strong
algorithm like HMAC-SHA256 with a per-token random salt or bcrypt/Argon2) rather
than the plaintext, ensure uniqueness is enforced on the hash if desired, and
update the code paths that generate and validate refresh tokens to (1) return
the raw token to the client but only persist its hash in RefreshToken, and (2)
on refresh requests hash the incoming token the same way and compare hashes to
authenticate; also ensure salts/pepper and secure random generation of the
original token are handled in the token creation routine and that any repository
queries (e.g., findByToken) are updated to query by the hashed value.

@@ -0,0 +1,15 @@
package com.example.booklog.global.auth.Repository;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Package naming convention: use lowercase.

The package name com.example.booklog.global.auth.Repository uses a capital "R" which violates Java naming conventions. Package names should be all lowercase.

♻️ Suggested fix
-package com.example.booklog.global.auth.Repository;
+package com.example.booklog.global.auth.repository;

Also update the directory structure and imports in other files accordingly.

🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/global/auth/Repository/RefreshTokenRepository.java`
at line 1, The package declaration in RefreshTokenRepository.java uses a
capitalized segment ("Repository") which breaks Java conventions; change the
package line from com.example.booklog.global.auth.Repository to
com.example.booklog.global.auth.repository, move the file into the matching
directory (repository) and update any import statements or references across the
codebase that point to com.example.booklog.global.auth.Repository (including
usages of the RefreshTokenRepository type) so they use
com.example.booklog.global.auth.repository instead.

Comment on lines +41 to +44
// RefreshToken 생성
public String createRefreshToken(CustomUserDetails user) {
return createToken(user, refreshExpiration);
}
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 | 🟠 Major

Security concern: Access and refresh tokens are indistinguishable.

Both token types share the same structure and signing key. A malicious actor could use a refresh token as an access token to bypass the intended token lifecycle. Consider adding a type claim to differentiate them.

🔒 Suggested fix
 // AccessToken 생성
 public String createAccessToken(CustomUserDetails user) {
-    return createToken(user, accessExpiration);
+    return createToken(user, accessExpiration, "access");
 }

 // RefreshToken 생성
 public String createRefreshToken(CustomUserDetails user) {
-    return createToken(user, refreshExpiration);
+    return createToken(user, refreshExpiration, "refresh");
 }

 // 토큰 생성
-private String createToken(CustomUserDetails user, Duration expiration) {
+private String createToken(CustomUserDetails user, Duration expiration, String tokenType) {
     Instant now = Instant.now();

     String authorities = user.getAuthorities().stream()
             .map(GrantedAuthority::getAuthority)
             .collect(Collectors.joining(","));

     return Jwts.builder()
             .subject(user.getUsername())
             .claim("role", authorities)
             .claim("email", user.getUsername())
+            .claim("type", tokenType)
             .issuedAt(Date.from(now))
             .expiration(Date.from(now.plus(expiration)))
             .signWith(secretKey)
             .compact();
 }

Then validate the token type in JwtAuthFilter and refreshToken flow.

🤖 Prompt for AI Agents
In `@booklog/src/main/java/com/example/booklog/global/auth/security/JwtUtil.java`
around lines 41 - 44, The refresh and access tokens are indistinguishable;
update the token creation flow so createRefreshToken (which calls createToken)
adds a claim like "type":"refresh" (and ensure access tokens include
"type":"access"); then update JwtAuthFilter and the refreshToken handling logic
to validate that incoming tokens have the expected "type" claim (e.g.,
JwtAuthFilter only accepts "access" and the refresh endpoint accepts "refresh")
and reject tokens with the wrong type. Ensure the claim key is consistent across
token issuance and validation and include tests for both
createRefreshToken/createToken, JwtAuthFilter, and the refreshToken flow.

Comment on lines +103 to +105
// 8. DB의 RefreshToken 업데이트
storedToken.updateToken(newRefreshToken, java.time.LocalDateTime.now().plus(jwtUtil.getRefreshExpiration()));
refreshTokenRepository.save(storedToken);
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

Potential race condition during concurrent token refresh.

If two concurrent requests attempt to refresh the same token, both may pass validation before either completes the update, potentially leading to inconsistent state. Consider using optimistic locking on the RefreshToken entity or a unique constraint on the token column.

♻️ Suggested approach using optimistic locking

Add a version field to RefreshToken entity:

`@Version`
private Long version;

This will cause concurrent updates to fail with OptimisticLockException, which you can catch and return an appropriate error.

🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/global/auth/service/AuthQueryServiceImpl.java`
around lines 103 - 105, The current refresh flow in AuthQueryServiceImpl updates
storedToken via storedToken.updateToken(...) and
refreshTokenRepository.save(storedToken) which can race under concurrent
requests; add optimistic locking to the RefreshToken entity by adding a `@Version`
Long version field (and optionally ensure a unique constraint on the token
column), then wrap the save/update in AuthQueryServiceImpl with handling for
OptimisticLockException (catch it and translate to a controlled error/retry
response) so concurrent refresh attempts fail fast and are handled cleanly.

@icarus0616 icarus0616 merged commit 395694e into dev Jan 28, 2026
1 check passed
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.

2 participants