-
Notifications
You must be signed in to change notification settings - Fork 0
3sp qa사항 반영 #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
3sp qa사항 반영 #154
Changes from all commits
12b00cf
219be6d
ff912b5
1e73655
2904c46
f034d6f
28b619b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.pinback.api.auth.controller; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.ResponseCookie; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.CookieValue; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import com.pinback.application.auth.dto.SignUpResponseV3; | ||
| import com.pinback.application.auth.dto.TokenResponse; | ||
| import com.pinback.application.auth.usecase.AuthUsecase; | ||
| import com.pinback.shared.dto.ResponseDto; | ||
|
|
||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/v3/auth") | ||
| @RequiredArgsConstructor | ||
| @Tag(name = "Authentication V3", description = "인증 관리 API V3") | ||
| public class AuthControllerV3 { | ||
| private final AuthUsecase authUsecase; | ||
|
|
||
| @Value("${jwt.refreshExpirationPeriod}") | ||
| private long refreshTokenExpirationPeriod; | ||
|
|
||
| @PostMapping("/reissue") | ||
| public ResponseEntity<ResponseDto<TokenResponse>> reissueAccessToken( | ||
| @CookieValue(name = "refreshToken") String refreshToken | ||
| ) { | ||
| SignUpResponseV3 response = authUsecase.getNewToken(refreshToken); | ||
|
|
||
| ResponseCookie cookie = ResponseCookie.from("refreshToken", response.refreshToken()) | ||
| .httpOnly(true) | ||
| .secure(true) | ||
| .path("/") | ||
| .maxAge(refreshTokenExpirationPeriod / 1000) | ||
| .sameSite("None") | ||
| .build(); | ||
|
|
||
| return ResponseEntity.ok() | ||
| .header(HttpHeaders.SET_COOKIE, cookie.toString()) | ||
| .body(ResponseDto.ok(TokenResponse.of(response.accessToken()))); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,9 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package com.pinback.api.google.controller; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.beans.factory.annotation.Value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.http.HttpHeaders; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.http.ResponseCookie; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.http.ResponseEntity; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.PatchMapping; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.PostMapping; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.RequestBody; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -8,7 +12,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.pinback.api.auth.dto.request.SignUpRequestV3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.pinback.api.google.dto.request.GoogleLoginRequestV3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.pinback.application.auth.dto.SignUpResponse; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.pinback.application.auth.dto.SignUpResponseV3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.pinback.application.auth.usecase.AuthUsecase; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.pinback.application.google.dto.response.GoogleLoginResponseV3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.pinback.application.google.usecase.GoogleUsecase; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -27,28 +31,51 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class GoogleLoginControllerV3 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final GoogleUsecase googleUsecase; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final AuthUsecase authUsecase; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Value("${jwt.refreshExpirationPeriod}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private long refreshTokenExpirationPeriod; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Operation(summary = "구글 소셜 로그인 V3", description = "구글 소셜 로그인을 진행하며, 응답에 직무 선택 여부를 포함합니다.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @PostMapping("/google") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public Mono<ResponseDto<GoogleLoginResponseV3>> googleLogin( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public Mono<ResponseEntity<ResponseDto<GoogleLoginResponseV3>>> googleLogin( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Valid @RequestBody GoogleLoginRequestV3 request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return googleUsecase.getUserInfoV3(request.toCommand()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .flatMap(googleResponse -> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return authUsecase.getInfoAndTokenV3(googleResponse.email(), googleResponse.pictureUrl(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| googleResponse.name()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(loginResponse -> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ResponseDto.ok(loginResponse); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clear the cookie on temp-login responses instead of issuing 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Operation(summary = "신규 회원 온보딩 V3", description = "신규 회원의 기본 정보(직무 포함)를 등록합니다.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @PatchMapping("/signup") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ResponseDto<SignUpResponse> signUpV3( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ResponseEntity<ResponseDto<SignUpResponseV3>> signUpV3( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Valid @RequestBody SignUpRequestV3 request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SignUpResponse response = authUsecase.signUpV3(request.toCommand()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ResponseDto.ok(response); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SignUpResponseV3 response = authUsecase.signUpV3(request.toCommand()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ResponseCookie cookie = ResponseCookie.from("refreshToken", response.refreshToken()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .httpOnly(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .secure(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .path("/") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .maxAge(refreshTokenExpirationPeriod / 1000) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .sameSite("None") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ResponseEntity.ok() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .header(HttpHeaders.SET_COOKIE, cookie.toString()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .body(ResponseDto.ok(response)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+77
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't return
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.pinback.application.auth.dto; | ||
|
|
||
| public record SignUpResponseV3( | ||
| String accessToken, | ||
| String refreshToken | ||
| ) { | ||
| public static SignUpResponseV3 from(String accessToken, String refreshToken) { | ||
| return new SignUpResponseV3(accessToken, refreshToken); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,20 @@ | ||
| package com.pinback.application.auth.usecase; | ||
|
|
||
| import java.time.LocalTime; | ||
| import java.util.UUID; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.data.redis.core.StringRedisTemplate; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import com.pinback.application.auth.dto.SignUpCommand; | ||
| import com.pinback.application.auth.dto.SignUpCommandV3; | ||
| import com.pinback.application.auth.dto.SignUpResponse; | ||
| import com.pinback.application.auth.dto.SignUpResponseV3; | ||
| import com.pinback.application.auth.dto.TokenResponse; | ||
| import com.pinback.application.auth.service.JwtProvider; | ||
| import com.pinback.application.common.exception.InvalidTokenException; | ||
| import com.pinback.application.config.ProfileImageConfig; | ||
| import com.pinback.application.google.dto.response.GoogleLoginResponse; | ||
| import com.pinback.application.google.dto.response.GoogleLoginResponseV3; | ||
|
|
@@ -30,6 +35,7 @@ | |
| @Service | ||
| @RequiredArgsConstructor | ||
| public class AuthUsecase { | ||
| private static final String REDIS_REFRESH_TOKEN_PREFIX = "RT:"; | ||
|
|
||
| private final UserValidateServicePort userValidateServicePort; | ||
| private final UserSaveServicePort userSaveServicePort; | ||
|
|
@@ -40,6 +46,10 @@ public class AuthUsecase { | |
| private final UserUpdateServicePort userUpdateServicePort; | ||
| private final UserOAuthUsecase userOAuthUsecase; | ||
| private final ProfileImageConfig profileImageConfig; | ||
| private final StringRedisTemplate stringRedisTemplate; | ||
|
|
||
| @Value("${jwt.refreshExpirationPeriod}") | ||
| private long refreshTokenExpirationPeriod; | ||
|
|
||
| @Transactional | ||
| public SignUpResponse signUp(SignUpCommand signUpCommand) { | ||
|
|
@@ -123,9 +133,11 @@ public SignUpResponse signUpV2(SignUpCommand signUpCommand) { | |
| } | ||
|
|
||
| @Transactional | ||
| public SignUpResponse signUpV3(SignUpCommandV3 signUpCommand) { | ||
| public SignUpResponseV3 signUpV3(SignUpCommandV3 signUpCommand) { | ||
| User user = userGetServicePort.findByEmail(signUpCommand.email()); | ||
| String accessToken = jwtProvider.createAccessToken(user.getId()); | ||
| String refreshToken = jwtProvider.createRefreshToken(user.getId()); | ||
| saveRefreshTokenToRedis(user.getId(), refreshToken); | ||
| userUpdateServicePort.updateRemindDefault(user.getId(), signUpCommand.remindDefault()); | ||
|
|
||
| savePushSubscriptionPort.savePushSubscription(user, signUpCommand.fcmToken()); | ||
|
|
@@ -135,7 +147,7 @@ public SignUpResponse signUpV3(SignUpCommandV3 signUpCommand) { | |
| Job job = Job.from(signUpCommand.job()); | ||
| userUpdateServicePort.updateJob(user.getId(), job); | ||
|
|
||
| return SignUpResponse.from(accessToken); | ||
| return SignUpResponseV3.from(accessToken, refreshToken); | ||
| } | ||
|
|
||
| @Transactional | ||
|
|
@@ -149,11 +161,15 @@ public Mono<GoogleLoginResponseV3> getInfoAndTokenV3(String email, String pictur | |
| if (updatedUser.getRemindDefault() != null && updatedUser.getProfileImage() != null) { | ||
| log.info("기존 사용자 로그인 성공: User ID {}", updatedUser.getId()); | ||
|
|
||
| //Access Token 발급 | ||
| //Access Token & Refresh Token 발급 | ||
| String accessToken = jwtProvider.createAccessToken(updatedUser.getId()); | ||
| String refreshToken = jwtProvider.createRefreshToken(updatedUser.getId()); | ||
|
|
||
| saveRefreshTokenToRedis(updatedUser.getId(), refreshToken); | ||
|
|
||
| return Mono.just(GoogleLoginResponseV3.loggedIn( | ||
| updatedUser.hasJob(), updatedUser.getId(), updatedUser.getEmail(), accessToken | ||
| updatedUser.hasJob(), updatedUser.getId(), updatedUser.getEmail(), accessToken, | ||
| refreshToken | ||
| )); | ||
| } else { | ||
| log.info("기존 사용자 - 온보딩 미완료 유저 처리: User ID {}", updatedUser.getId()); | ||
|
|
@@ -185,6 +201,25 @@ public Mono<GoogleLoginResponseV3> getInfoAndTokenV3(String email, String pictur | |
| })); | ||
| } | ||
|
|
||
| @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(); | ||
| } | ||
|
Comment on lines
+204
to
+213
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate the JWT type before deleting the Redis token.
|
||
|
|
||
| String newAccessToken = jwtProvider.createAccessToken(userId); | ||
| String newRefreshToken = jwtProvider.createRefreshToken(userId); | ||
|
|
||
| saveRefreshTokenToRedis(userId, newRefreshToken); | ||
|
|
||
| return SignUpResponseV3.from(newAccessToken, newRefreshToken); | ||
| } | ||
|
|
||
| private Mono<User> applyMissingUserInfo(User existingUser, String pictureUrl, String name) { | ||
| // 1. 이름 업데이트 | ||
| boolean nameUpdated = false; | ||
|
|
@@ -224,4 +259,13 @@ private String matchingProfileImage(LocalTime remindDefault) { | |
| return "IMAGE3"; | ||
| } | ||
| } | ||
|
|
||
| private void saveRefreshTokenToRedis(UUID userId, String refreshToken) { | ||
| stringRedisTemplate.opsForValue().set( | ||
| "RT:" + userId.toString(), | ||
| refreshToken, | ||
| refreshTokenExpirationPeriod, | ||
| java.util.concurrent.TimeUnit.MILLISECONDS | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.pinback.application.common.exception; | ||
|
|
||
| import com.pinback.shared.constant.ExceptionCode; | ||
| import com.pinback.shared.exception.ApplicationException; | ||
|
|
||
| public class InvalidTokenException extends ApplicationException { | ||
| public InvalidTokenException() { | ||
| super(ExceptionCode.INVALID_TOKEN); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,13 +7,15 @@ public record GoogleLoginResponseV3( | |
| boolean hasJob, | ||
| UUID userId, | ||
| String email, | ||
| String accessToken | ||
| 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); | ||
|
Comment on lines
+10
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| public static GoogleLoginResponseV3 tempLogin(UUID userId, String email) { | ||
| return new GoogleLoginResponseV3(false, false, userId, email, null); | ||
| return new GoogleLoginResponseV3(false, false, userId, email, null, null); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In Spring Framework CORS configuration, what are the security implications of usingsetAllowCredentials(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 explicitAccess-Control-Allow-Origin(not*) plusAccess-Control-Allow-Credentials: true. Spring notes that the CORS spec forbids*with credentials and providesallowedOriginPatternsas the “dynamic origins” alternative. [2]setAllowedOriginPatterns(List.of("chrome-extension://*"))matches any Chrome extension origin of the formchrome-extension://<extension-id>(the extension ID acts like the “host” part of the origin). [3] When anallowedOriginPatternmatches, Spring setsAccess-Control-Allow-Originto 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 thanchrome-extension://*. [1]🏁 Script executed:
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.javaRepository: 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