FEAT: 리프레시 토큰 방식 추가 & api 엔드포인트 변경#77
Conversation
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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>
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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 aClock(orInstant/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_SUCCESSfor the refresh endpoint works but is semantically imprecise. Consider adding aTOKEN_REFRESH_SUCCESScode 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 movingrefreshTokentoAuthCommandService.The
refreshTokenmethod performs mutations (token rotation, DB updates) which violates the query/command separation implied byAuthQueryService. While functionally correct, moving this toAuthCommandServicewould better reflect its behavior.
| @Schema(name = "AuthRefreshToeknRequest") | ||
| public record RefreshTokenDTO( |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "AuthReqDTO.java" | head -20Repository: Project-BookLog/BookLog-BackEnd
Length of output: 150
🏁 Script executed:
cat -n booklog/src/main/java/com/example/booklog/global/auth/dto/AuthReqDTO.javaRepository: 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.
| @Column(nullable = false, unique = true) | ||
| private String token; |
There was a problem hiding this comment.
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; | |||
There was a problem hiding this comment.
🛠️ 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.
| // RefreshToken 생성 | ||
| public String createRefreshToken(CustomUserDetails user) { | ||
| return createToken(user, refreshExpiration); | ||
| } |
There was a problem hiding this comment.
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.
| // 8. DB의 RefreshToken 업데이트 | ||
| storedToken.updateToken(newRefreshToken, java.time.LocalDateTime.now().plus(jwtUtil.getRefreshExpiration())); | ||
| refreshTokenRepository.save(storedToken); |
There was a problem hiding this comment.
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.
Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.