Skip to content

3sp qa사항 반영#154

Merged
ose0221 merged 7 commits intomainfrom
dev
Mar 10, 2026
Merged

3sp qa사항 반영#154
ose0221 merged 7 commits intomainfrom
dev

Conversation

@ose0221
Copy link
Contributor

@ose0221 ose0221 commented Mar 9, 2026

🚀 PR 요약

목적이 무엇인가요? - 지우고 작성

✨ PR 상세 내용

어떤 부분이 어떻게 변경이 되었나요? - 지우고 작성

🚨 주의 사항

주의할 부분이 무엇인가요? - 지우고 작성

✅ 체크 리스트

  • 리뷰어 설정했나요?
  • Label 설정했나요?
  • 제목 양식 맞췄나요? (ex. feat: 기능 추가)
  • 변경 사항에 대한 테스트를 진행했나요?

Summary by CodeRabbit

  • New Features

    • Added a token refresh endpoint and refreshed-session flow; login/signup now set secure, HttpOnly refresh-token cookies with SameSite=None and configurable expiration.
    • Refresh tokens are persisted and rotated to improve session reliability and security.
    • CORS updated to allow credentials for cross-origin authenticated requests.
  • Bug Fixes

    • Memo length validation adjusted so memos exactly at the limit are accepted.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

Adds Redis-backed refresh-token rotation: new AuthControllerV3 POST /api/v3/auth/reissue, AuthUsecase refresh validation/rotation, JwtProvider refresh token creation, updated login/signup to set secure HttpOnly refreshToken cookies, and security/filter updates to allow the reissue endpoint.

Changes

Cohort / File(s) Summary
Auth V3 Controller
api/src/main/java/com/pinback/api/auth/controller/AuthControllerV3.java
New controller exposing POST /api/v3/auth/reissue; reads refreshToken cookie, calls AuthUsecase.getNewToken(...), returns access token body and sets rotated refresh token as secure HttpOnly cookie.
Google Auth Endpoints
api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java
googleLogin and signUpV3 now return ResponseEntity with Set-Cookie for refreshToken; added @Value refreshTokenExpirationPeriod and switched to SignUpResponseV3.
Security & Filter
api/src/main/java/com/pinback/api/config/filter/JwtAuthenticationFilter.java, api/src/main/java/com/pinback/api/config/security/SecurityConfig.java
Excludes /api/v3/auth/reissue from JWT filter and adds the reissue path to permit-list.
Auth Usecase & Redis
application/src/main/java/com/pinback/application/auth/usecase/AuthUsecase.java
Adds Redis-backed refresh token storage and rotation; signUpV3 and token flows now produce/persist refresh tokens; new getNewToken(String) validates, rotates, and saves refresh tokens.
JWT Provider
application/src/main/java/com/pinback/application/auth/service/JwtProvider.java, infrastructure/src/main/java/com/pinback/infrastructure/jwt/JwtProviderImpl.java
Adds createRefreshToken(UUID) to interface and implements it; JwtProviderImpl accepts jwt.refreshExpirationPeriod and issues refresh tokens with separate subject/expiry.
DTOs & Exceptions
application/src/main/java/com/pinback/application/auth/dto/SignUpResponseV3.java, application/src/main/java/com/pinback/application/auth/dto/TokenResponse.java, application/src/main/java/com/pinback/application/common/exception/InvalidTokenException.java
Adds SignUpResponseV3 record, TokenResponse.of() factory, and new InvalidTokenException.
Google Response DTO
application/src/main/java/com/pinback/application/google/dto/response/GoogleLoginResponseV3.java
Adds refreshToken field and updates factory methods to include it.
Config & Dependency
api/src/main/resources/application.yml, application/build.gradle
Adds jwt.refreshExpirationPeriod property and spring-boot-starter-data-redis dependency.
Article Query
infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java
Eager-fetches user via fetchJoin(), joins category, and filters results to public categories.
Validation Tweaks
application/src/main/java/com/pinback/application/article/usecase/command/CreateArticleUsecase.java, .../UpdateArticleUsecase.java
Adjusts memo-length boundary checks from >= to > so length exactly at limit is allowed.
CORS
api/src/main/java/com/pinback/api/config/cors/CorsConfig.java
Allows credentials in CORS configuration (credentials: true).

Sequence Diagram

sequenceDiagram
    participant Client
    participant AuthController as AuthControllerV3
    participant AuthUsecase
    participant JwtProvider
    participant Redis
    participant Response

    Client->>AuthController: POST /api/v3/auth/reissue (refreshToken cookie)
    AuthController->>AuthUsecase: getNewToken(refreshToken)
    AuthUsecase->>Redis: lookup stored refresh token
    Redis-->>AuthUsecase: stored refresh token
    AuthUsecase->>AuthUsecase: validate provided == stored
    alt valid
        AuthUsecase->>JwtProvider: createAccessToken(userId)
        JwtProvider-->>AuthUsecase: accessToken
        AuthUsecase->>JwtProvider: createRefreshToken(userId)
        JwtProvider-->>AuthUsecase: newRefreshToken
        AuthUsecase->>Redis: store newRefreshToken with TTL
        Redis-->>AuthUsecase: ack
        AuthUsecase-->>AuthController: tokens (accessToken, newRefreshToken)
    else invalid
        AuthUsecase-->>AuthController: throw InvalidTokenException
    end
    AuthController->>Response: Build ResponseEntity + Set-Cookie (refreshToken HttpOnly Secure SameSite=None) + body (accessToken)
    Response-->>Client: HTTP 200 with cookie
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

feat

Suggested reviewers

  • rootTiket

Poem

🐰 I nibbled bytes and hopped through code,

I spun a token's secret road,
Redis cradles keys I sow,
Cookies guard where breezes blow,
Fresh tokens ready — hop, off you go!

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR description is entirely placeholder text with no actual implementation details, cautions, or testing information provided. Complete all required sections: summarize refresh token and category filtering changes, document setup requirements, note testing performed, and explain any breaking changes.
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.
Title check ❓ Inconclusive The title '3sp qa사항 반영' is vague and uses non-descriptive Korean phrasing that does not clearly convey the substantive changes in the changeset. Revise the title to clearly summarize the main changes, such as 'Add refresh token functionality and restrict article sharing to public categories' or provide a more specific description of the QA items addressed.

✏️ 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 dev

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

Copy link

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
infrastructure/src/main/java/com/pinback/infrastructure/jwt/JwtProviderImpl.java (1)

36-46: ⚠️ Potential issue | 🔴 Critical

Critical: Thread-safety issue with shared JWTCreator.Builder.

The jwtBuilder field is shared across all method calls, but JWTCreator.Builder is mutable. Methods like withClaim(), withSubject(), and withExpiresAt() mutate the builder's internal state. When createAccessToken and createRefreshToken are called concurrently (common in a web application), the builder state will be corrupted, producing tokens with incorrect claims or subjects.

Create a fresh builder for each token instead:

🔒 Proposed fix
 `@Component`
 public class JwtProviderImpl implements JwtProvider {
 	private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
 	private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
 	private static final String ID_CLAIM = "id";

 	private final String key;
 	private final long accessExpirationPeriod;
 	private final long refreshTokenExpirationPeriod;
-	private final JWTCreator.Builder jwtBuilder;
+	private final String issuer;

 	public JwtProviderImpl(`@Value`("${jwt.secret-key}") String key,
 		`@Value`("${jwt.accessExpirationPeriod}") long accessExpirationPeriod,
 		`@Value`("${jwt.refreshExpirationPeriod}") long refreshTokenExpirationPeriod,
 		`@Value`("${jwt.issuer}") String issuer) {

 		this.key = key;
 		this.accessExpirationPeriod = accessExpirationPeriod;
 		this.refreshTokenExpirationPeriod = refreshTokenExpirationPeriod;
-		this.jwtBuilder = JWT.create()
-			.withIssuer(issuer);
+		this.issuer = issuer;
 	}

 	`@Override`
 	public String createAccessToken(UUID id) {
-		return jwtBuilder
+		return JWT.create()
+			.withIssuer(issuer)
 			.withClaim(ID_CLAIM, id.toString())
 			.withSubject(ACCESS_TOKEN_SUBJECT)
 			.withExpiresAt(Instant.now().plusMillis(accessExpirationPeriod))
 			.sign(Algorithm.HMAC512(key));
 	}

 	`@Override`
 	public String createRefreshToken(UUID userId) {
-		return jwtBuilder
+		return JWT.create()
+			.withIssuer(issuer)
 			.withClaim(ID_CLAIM, userId.toString())
 			.withSubject(REFRESH_TOKEN_SUBJECT)
 			.withExpiresAt(Instant.now().plusMillis(refreshTokenExpirationPeriod))
 			.sign(Algorithm.HMAC512(key));
 	}

Also applies to: 66-73

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

In
`@infrastructure/src/main/java/com/pinback/infrastructure/jwt/JwtProviderImpl.java`
around lines 36 - 46, The jwtBuilder field is a shared, mutable
JWTCreator.Builder causing race conditions; instead of mutating the shared
jwtBuilder in createAccessToken and createRefreshToken, instantiate a fresh
builder from JWT.create() inside each method (e.g., within
createAccessToken(UUID id) and createRefreshToken(UUID id)) and then call
withClaim(ID_CLAIM,...), withSubject(...), withExpiresAt(...) and
sign(Algorithm.HMAC512(key)) on that new builder so each call uses an
independent builder instance.
api/src/main/java/com/pinback/api/config/security/SecurityConfig.java (1)

54-61: ⚠️ Potential issue | 🔴 Critical

Fix the missing leading slash in the permitAll path for /api/v3/auth/reissue (line 60).

Line 60 has "api/v3/auth/reissue" without the leading slash. Spring Security expects absolute, slash-prefixed request paths in requestMatchers(String...), so this rule will not match incoming requests to /api/v3/auth/reissue. The request falls through to .anyRequest().authenticated(), rejecting anonymous reissue calls. All other paths in this block (lines 55–59) correctly include the leading slash. JwtAuthenticationFilter.java:108 also validates the path with the leading slash (path.startsWith("/api/v3/auth/reissue")), confirming the expected format. (Spring Security Docs)

🔧 Proposed fix
 				.requestMatchers(
 					"/api/v1/auth/token",
 					"/api/v2/auth/google",
 					"/api/v2/auth/signup",
 					"/api/v3/auth/signup",
 					"/api/v3/auth/google",
-					"api/v3/auth/reissue"
+					"/api/v3/auth/reissue"
 				).permitAll()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/main/java/com/pinback/api/config/security/SecurityConfig.java` around
lines 54 - 61, In SecurityConfig update the request matcher entry that currently
reads "api/v3/auth/reissue" to include the leading slash so it matches incoming
requests; locate the requestMatchers(...) call inside SecurityConfig and make
the string "/api/v3/auth/reissue" consistent with the other entries so
permitAll() applies; verify this aligns with the path check in
JwtAuthenticationFilter (path.startsWith("/api/v3/auth/reissue")) to ensure
anonymous reissue calls are allowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java`:
- Around line 77-79: The controller currently returns SignUpResponseV3 (which
contains refreshToken) in the response body after setting the refresh token
cookie; change GoogleLoginControllerV3 to not expose SignUpResponseV3 directly
but instead construct and return a TokenResponse (or a DTO that excludes
refreshToken) in the body while keeping the refresh token only in the HttpOnly
cookie (use the existing cookie variable and map the accessToken and other
non-secret fields from SignUpResponseV3 into TokenResponse); ensure the response
body type and returned object match TokenResponse to mirror AuthControllerV3
behavior.
- Around line 47-57: The response currently always sets a refreshToken cookie in
GoogleLoginControllerV3 even when AuthUsecase.getInfoAndTokenV3() returns a
tempLogin (GoogleLoginResponseV3.tempLogin() sets refreshToken to null); change
the controller to check loginResponse.refreshToken() and if null send a clearing
Set-Cookie (e.g., cookie with empty value and maxAge=0/expired) instead of
issuing a normal refresh cookie so onboarding-incomplete/new-user flows do not
leave a stale or meaningless cookie.

In
`@application/src/main/java/com/pinback/application/google/dto/response/GoogleLoginResponseV3.java`:
- Around line 10-15: GoogleLoginResponseV3 currently includes a refreshToken
field and the loggedIn(...) factory returns it, causing the refresh token to be
serialized into the JSON response; remove the refreshToken from the record
declaration and update the loggedIn method signature and body to omit the
refreshToken parameter so JSON responses no longer include it (keep sending the
refresh token only via the HttpOnly cookie in GoogleLoginControllerV3). Ensure
all references to GoogleLoginResponseV3.refreshToken and the loggedIn overload
are updated or removed accordingly to avoid compilation errors.

---

Outside diff comments:
In `@api/src/main/java/com/pinback/api/config/security/SecurityConfig.java`:
- Around line 54-61: In SecurityConfig update the request matcher entry that
currently reads "api/v3/auth/reissue" to include the leading slash so it matches
incoming requests; locate the requestMatchers(...) call inside SecurityConfig
and make the string "/api/v3/auth/reissue" consistent with the other entries so
permitAll() applies; verify this aligns with the path check in
JwtAuthenticationFilter (path.startsWith("/api/v3/auth/reissue")) to ensure
anonymous reissue calls are allowed.

In
`@infrastructure/src/main/java/com/pinback/infrastructure/jwt/JwtProviderImpl.java`:
- Around line 36-46: The jwtBuilder field is a shared, mutable
JWTCreator.Builder causing race conditions; instead of mutating the shared
jwtBuilder in createAccessToken and createRefreshToken, instantiate a fresh
builder from JWT.create() inside each method (e.g., within
createAccessToken(UUID id) and createRefreshToken(UUID id)) and then call
withClaim(ID_CLAIM,...), withSubject(...), withExpiresAt(...) and
sign(Algorithm.HMAC512(key)) on that new builder so each call uses an
independent builder instance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 83cf2429-c532-49e9-9518-c0d94e215862

📥 Commits

Reviewing files that changed from the base of the PR and between 318768b and 1e73655.

📒 Files selected for processing (14)
  • api/src/main/java/com/pinback/api/auth/controller/AuthControllerV3.java
  • api/src/main/java/com/pinback/api/config/filter/JwtAuthenticationFilter.java
  • api/src/main/java/com/pinback/api/config/security/SecurityConfig.java
  • api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java
  • api/src/main/resources/application.yml
  • application/build.gradle
  • application/src/main/java/com/pinback/application/auth/dto/SignUpResponseV3.java
  • application/src/main/java/com/pinback/application/auth/dto/TokenResponse.java
  • application/src/main/java/com/pinback/application/auth/service/JwtProvider.java
  • application/src/main/java/com/pinback/application/auth/usecase/AuthUsecase.java
  • application/src/main/java/com/pinback/application/common/exception/InvalidTokenException.java
  • application/src/main/java/com/pinback/application/google/dto/response/GoogleLoginResponseV3.java
  • infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java
  • infrastructure/src/main/java/com/pinback/infrastructure/jwt/JwtProviderImpl.java

Comment on lines +47 to +57
ResponseCookie cookie = ResponseCookie.from("refreshToken", loginResponse.refreshToken())
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(refreshTokenExpirationPeriod / 1000)
.sameSite("None")
.build();

return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(ResponseDto.ok(loginResponse));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear the cookie on temp-login responses instead of issuing one.

AuthUsecase.getInfoAndTokenV3() returns GoogleLoginResponseV3.tempLogin(...) for onboarding-incomplete and new users in application/src/main/java/com/pinback/application/auth/usecase/AuthUsecase.java, Line 177-179 and Line 198-199, and tempLogin() sets refreshToken to null in application/src/main/java/com/pinback/application/google/dto/response/GoogleLoginResponseV3.java, Line 18-19. Line 47-57 still writes a refreshToken cookie for that branch, so the non-user path can leave a stale cookie behind or emit a meaningless one.

🔧 Proposed fix
 					.map(loginResponse -> {
-						ResponseCookie cookie = ResponseCookie.from("refreshToken", loginResponse.refreshToken())
-							.httpOnly(true)
-							.secure(true)
-							.path("/")
-							.maxAge(refreshTokenExpirationPeriod / 1000)
-							.sameSite("None")
-							.build();
+						ResponseCookie cookie = loginResponse.refreshToken() == null
+							? ResponseCookie.from("refreshToken", "")
+								.httpOnly(true)
+								.secure(true)
+								.path("/")
+								.maxAge(0)
+								.sameSite("None")
+								.build()
+							: ResponseCookie.from("refreshToken", loginResponse.refreshToken())
+								.httpOnly(true)
+								.secure(true)
+								.path("/")
+								.maxAge(refreshTokenExpirationPeriod / 1000)
+								.sameSite("None")
+								.build();
 
 						return ResponseEntity.ok()
 							.header(HttpHeaders.SET_COOKIE, cookie.toString())
 							.body(ResponseDto.ok(loginResponse));
 					});
📝 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
ResponseCookie cookie = ResponseCookie.from("refreshToken", loginResponse.refreshToken())
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(refreshTokenExpirationPeriod / 1000)
.sameSite("None")
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(ResponseDto.ok(loginResponse));
.map(loginResponse -> {
ResponseCookie cookie = loginResponse.refreshToken() == null
? ResponseCookie.from("refreshToken", "")
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(0)
.sameSite("None")
.build()
: ResponseCookie.from("refreshToken", loginResponse.refreshToken())
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(refreshTokenExpirationPeriod / 1000)
.sameSite("None")
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(ResponseDto.ok(loginResponse));
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java`
around lines 47 - 57, The response currently always sets a refreshToken cookie
in GoogleLoginControllerV3 even when AuthUsecase.getInfoAndTokenV3() returns a
tempLogin (GoogleLoginResponseV3.tempLogin() sets refreshToken to null); change
the controller to check loginResponse.refreshToken() and if null send a clearing
Set-Cookie (e.g., cookie with empty value and maxAge=0/expired) instead of
issuing a normal refresh cookie so onboarding-incomplete/new-user flows do not
leave a stale or meaningless cookie.

Comment on lines +77 to +79
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(ResponseDto.ok(response));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't return SignUpResponseV3 verbatim after setting the cookie.

SignUpResponseV3 includes refreshToken in application/src/main/java/com/pinback/application/auth/dto/SignUpResponseV3.java, Line 1-10, so Line 77-79 still exposes the secret in JSON. AuthControllerV3 already shows the safer contract on Line 44-46 by returning only TokenResponse.

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

In
`@api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java`
around lines 77 - 79, The controller currently returns SignUpResponseV3 (which
contains refreshToken) in the response body after setting the refresh token
cookie; change GoogleLoginControllerV3 to not expose SignUpResponseV3 directly
but instead construct and return a TokenResponse (or a DTO that excludes
refreshToken) in the body while keeping the refresh token only in the HttpOnly
cookie (use the existing cookie variable and map the accessToken and other
non-secret fields from SignUpResponseV3 into TokenResponse); ensure the response
body type and returned object match TokenResponse to mirror AuthControllerV3
behavior.

Comment on lines +204 to +213
@Transactional
public SignUpResponseV3 getNewToken(String refreshToken) {
UUID userId = jwtProvider.getUserIdFromToken(refreshToken);

String redisKey = REDIS_REFRESH_TOKEN_PREFIX + userId.toString();
String savedToken = stringRedisTemplate.opsForValue().get(redisKey);
if (savedToken == null || !savedToken.equals(refreshToken)) {
stringRedisTemplate.delete(redisKey);
throw new InvalidTokenException();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the JWT type before deleting the Redis token.

jwtProvider.getUserIdFromToken(...) in infrastructure/src/main/java/com/pinback/infrastructure/jwt/JwtProviderImpl.java, Line 60-64 extracts the user ID from any valid JWT. If an access token is sent here, Line 210-212 will reject it and delete the real refresh token, turning a bad request into a forced logout. Only revoke on mismatch after you've confirmed the presented JWT is actually a refresh token.

Comment on lines +10 to +15
String accessToken,
String refreshToken
) {
public static GoogleLoginResponseV3 loggedIn(boolean hasJob, UUID userId, String email, String accessToken) {
return new GoogleLoginResponseV3(true, hasJob, userId, email, accessToken);
public static GoogleLoginResponseV3 loggedIn(boolean hasJob, UUID userId, String email, String accessToken,
String refreshToken) {
return new GoogleLoginResponseV3(true, hasJob, userId, email, accessToken, refreshToken);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep refreshToken off this response record.

GoogleLoginControllerV3.googleLogin() serializes GoogleLoginResponseV3 directly in api/src/main/java/com/pinback/api/google/controller/GoogleLoginControllerV3.java, Line 55-57. Adding refreshToken here makes it readable in the JSON body even though the controller also sends it as an HttpOnly cookie.

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

In
`@application/src/main/java/com/pinback/application/google/dto/response/GoogleLoginResponseV3.java`
around lines 10 - 15, GoogleLoginResponseV3 currently includes a refreshToken
field and the loggedIn(...) factory returns it, causing the refresh token to be
serialized into the JSON response; remove the refreshToken from the record
declaration and update the loggedIn method signature and body to omit the
refreshToken parameter so JSON responses no longer include it (keep sending the
refresh token only via the HttpOnly cookie in GoogleLoginControllerV3). Ensure
all references to GoogleLoginResponseV3.refreshToken and the loggedIn overload
are updated or removed accordingly to avoid compilation errors.

Copy link

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/main/java/com/pinback/api/config/cors/CorsConfig.java`:
- Line 49: The CorsConfig currently calls
configuration.setAllowCredentials(true) while also registering the
chrome-extension://* origin pattern; change this so credentialed CORS is never
enabled for a wildcard extension origin: create two distinct CorsConfiguration
entries in CorsConfig — one for chrome-extension://* (or extension traffic) with
setAllowCredentials(false) and the minimal allowed methods/headers, and a
separate credentialed configuration that only lists explicit allowedOrigins (no
wildcards) and keeps setAllowCredentials(true); update the
registerCorsConfiguration calls so chrome-extension://* uses the
non-credentialed CorsConfiguration and only specific extension IDs or trusted
origins are placed in the credentialed CorsConfiguration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b57d8a53-9803-41d7-879e-e581fc2e3531

📥 Commits

Reviewing files that changed from the base of the PR and between f034d6f and 28b619b.

📒 Files selected for processing (2)
  • api/src/main/java/com/pinback/api/config/cors/CorsConfig.java
  • api/src/main/java/com/pinback/api/config/security/SecurityConfig.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/main/java/com/pinback/api/config/security/SecurityConfig.java

));

configuration.setAllowCredentials(false);
configuration.setAllowCredentials(true);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In Spring Framework CORS configuration, what are the security implications of using setAllowCredentials(true)together withsetAllowedOriginPatterns(List.of("chrome-extension://*"))? Does this allow any Chrome extension origin to make credentialed cross-origin requests?

💡 Result:

Using setAllowCredentials(true) means the server is telling the browser it’s OK for the JS caller to make credentialed cross-origin requests (cookies / HTTP auth / client certs, depending on the client) and read the response, as long as the response includes an explicit Access-Control-Allow-Origin (not *) plus Access-Control-Allow-Credentials: true. Spring notes that the CORS spec forbids * with credentials and provides allowedOriginPatterns as the “dynamic origins” alternative. [2]

setAllowedOriginPatterns(List.of("chrome-extension://*")) matches any Chrome extension origin of the form chrome-extension://<extension-id> (the extension ID acts like the “host” part of the origin). [3] When an allowedOriginPattern matches, Spring sets Access-Control-Allow-Origin to the request’s Origin value (the matched origin), not to * and not to the pattern. [1] That makes it fully effective for credentialed requests.

So yes: for requests that are subject to normal browser CORS enforcement, this configuration effectively allows any Chrome extension ID to make credentialed cross-origin requests to your Spring app and read the responses, as long as the extension uses credentials: "include" (or equivalent) and the browser includes the user’s cookies for your site.

Security implication: you are extending “trusted frontend” status to every installed extension (including potentially malicious ones), which can then exfiltrate user-specific data from your API using the user’s ambient authentication (cookies/session). Prefer whitelisting specific extension IDs (e.g., chrome-extension://<your-id>) rather than chrome-extension://*. [1]


🏁 Script executed:

# First, locate and read the CorsConfig.java file
find . -name "CorsConfig.java" -type f | head -20

Repository: Pinback-Team/pinback-server

Length of output: 134


🏁 Script executed:

# Read the CorsConfig.java file with line numbers
cat -n ./api/src/main/java/com/pinback/api/config/cors/CorsConfig.java

Repository: Pinback-Team/pinback-server

Length of output: 2183


Do not enable credentialed CORS for the wildcard extension origin.

Line 49 enables credentials for the chrome-extension://* pattern defined at line 22, allowing any installed Chrome extension to make authenticated cross-origin requests and read responses with the user's cookies. Split extension traffic into a separate non-credentialed CORS policy, or whitelist only the specific extension IDs that require authenticated access.

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

In `@api/src/main/java/com/pinback/api/config/cors/CorsConfig.java` at line 49,
The CorsConfig currently calls configuration.setAllowCredentials(true) while
also registering the chrome-extension://* origin pattern; change this so
credentialed CORS is never enabled for a wildcard extension origin: create two
distinct CorsConfiguration entries in CorsConfig — one for chrome-extension://*
(or extension traffic) with setAllowCredentials(false) and the minimal allowed
methods/headers, and a separate credentialed configuration that only lists
explicit allowedOrigins (no wildcards) and keeps setAllowCredentials(true);
update the registerCorsConfiguration calls so chrome-extension://* uses the
non-credentialed CorsConfiguration and only specific extension IDs or trusted
origins are placed in the credentialed CorsConfiguration.

@ose0221 ose0221 changed the title feat: 공개된 카테고리의 아티클만 공유하도록 기능 수정 3sp qa사항 반영 Mar 10, 2026
@ose0221 ose0221 merged commit 99a8686 into main Mar 10, 2026
8 checks 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.

1 participant