Skip to content

Conversation

@Gwonwoo-Nam
Copy link
Contributor

@Gwonwoo-Nam Gwonwoo-Nam commented Nov 29, 2025

🟢 구현내용

  1. 사용자가 Apple 로그인 버튼 클릭 → Apple 로그인 페이지
  2. Apple이 https://api.upbrella.co.kr/auth/apple로 POST (code 전달)
  3. 백엔드에서 토큰 발급 및 ID Token 검증
  4. 세션에 appleUser 저장 후 https://upbrella.co.kr/login?apple=success로 리다이렉트
  5. 프론트엔드가 쿼리 파라미터 감지하여 /users/login 호출
  6. 회원 존재 여부에 따라 로그인 or 회원가입 페이지로 이동

🧩 고민과 해결과정

Summary by CodeRabbit

  • New Features

    • Apple Sign-In is now available alongside Kakao for login and account creation.
  • Improvements

    • Social login flows unified with consistent success/error messaging and multi-provider session handling.
    • Added server-side support for Apple token processing and client-secret generation.
    • Production cookie and CORS settings adjusted for secure Apple OAuth interactions.
  • Tests

    • Test fixtures, resources, and mocks expanded to cover Apple OAuth scenarios.

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

@coderabbitai
Copy link

coderabbitai bot commented Nov 29, 2025

Walkthrough

Added end-to-end Apple OAuth support: new JJWT/BouncyCastle deps, Apple client-secret JWT generator, Apple ID token decoder, DTOs and OAuth flows in service/controller, User provider field and Apple-specific user creation, session/CORS/auth config updates, and test wiring/resources.

Changes

Cohort / File(s) Summary
Build / Crypto Dependencies
build.gradle
Added JJWT libs (io.jsonwebtoken:jjwt-api:0.11.5, jjwt-impl & jjwt-jackson as runtimeOnly) and BouncyCastle (org.bouncycastle:bcpkix-jdk15on:1.70).
Apple OAuth Config & Secrets
src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt
New AppleOauthInfo interface and profile-specific components that generate clientSecret at startup via AppleJwtGenerator.
Apple JWT Generator
src/main/kotlin/upbrella/be/util/AppleJwtGenerator.kt
New component that loads a PKCS#8 p8 key (PEM) and builds an ES256-signed Apple client-secret JWT (uses JJWT + BouncyCastle).
Apple ID Token Decoder
src/main/kotlin/upbrella/be/util/AppleIdTokenDecoder.kt
New component to fetch Apple public keys and validate/decode Apple ID tokens into AppleLoginResponse.
OAuth Token / Login Service
src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt
Added Apple overloads for token exchange, centralized token helpers, and processAppleLogin(idToken) to decode Apple ID tokens.
User Service
src/main/kotlin/upbrella/be/user/service/UserService.kt
Added loginApple(appleSub: String) and joinApple(appleUser: AppleLoginResponse, joinRequest: JoinRequest) to handle Apple flows.
User Entity & DTOs
src/main/kotlin/upbrella/be/user/entity/User.kt, src/main/kotlin/upbrella/be/user/dto/response/AppleLoginResponse.kt, src/main/kotlin/upbrella/be/user/dto/token/OauthToken.kt
Added provider field to User (default "KAKAO"), createNewAppleUser factory; new AppleLoginResponse DTO; added idToken: String? to OauthToken.
User Controller & Auth Paths
src/main/kotlin/upbrella/be/user/controller/UserController.kt, src/main/kotlin/upbrella/be/config/AuthConfig.kt
Controller now accepts AppleOauthInfo, handles Apple OAuth callback (/auth/apple/**) and session flows; auth interceptor excludes /auth/apple/**.
CORS / Session Cookie Config
src/main/kotlin/upbrella/be/config/ProductionCorsConfig.kt, src/main/kotlin/upbrella/be/config/SessionCookieConfig.kt, src/main/resources/application-prod.yml, src/main/resources/application-dev.yml
Added Apple origin to allowedOrigins, new prod SessionCookieConfig (secure, HttpOnly, domain), and session cookie settings in YAML (same-site changes).
Tests & Test Resources
src/test/kotlin/.../*, src/test/resources/application-test.yml, src/test/resources/keys/AuthKey_TEST.p8
Wired/mocked AppleOauthInfo and AppleIdTokenDecoder in tests, updated controller/integration test setup, added Apple test config keys and a test p8 private key; updated many tests to use named User args including provider="KAKAO".

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant UC as UserController
    participant OLS as OauthLoginService
    participant AppleAPI as Apple OAuth API
    participant AID as AppleIdTokenDecoder
    participant US as UserService
    participant DB as Database

    Note over Client,UC: Apple OAuth authorization code flow -> token -> id_token decode -> session
    Client->>UC: GET /auth/apple/callback?code=CODE
    UC->>OLS: getOauthToken(CODE, appleOauthInfo)
    OLS->>AppleAPI: POST /auth/token (code, client_id, client_secret, redirect_uri)
    AppleAPI-->>OLS: OauthToken { access_token, id_token, ... }
    OLS->>AID: processAppleLogin(id_token)
    AID-->>OLS: AppleLoginResponse { sub, email, email_verified }
    OLS-->>UC: AppleLoginResponse
    UC->>UC: store AppleLoginResponse in session (appleUser)
    UC-->>Client: 302 redirect to frontend with success/failure

    Note over Client,UC: Later user action -> login/join uses session-stored AppleLoginResponse
    Client->>UC: POST /users/login (session contains appleUser)
    UC->>US: loginApple(sub)
    US->>DB: find user by apple-sub-hash / create if joining
    DB-->>US: SessionUser
    US-->>UC: SessionUser
    UC->>UC: clear session attrs
    UC-->>Client: Login success response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas needing extra attention:
    • AppleJwtGenerator.kt — PEM loading, key parsing, ES256 signing, and classpath p8 handling.
    • AppleIdTokenDecoder.kt — public key fetching, JWT header parsing, RSA key generation and verification, and error paths.
    • OauthLoginService/UserController — correct branching for Kakao vs Apple flows and session attribute lifecycle.
    • User.createNewAppleUser / provider handling — consistency with existing encryption and user creation flows.

Possibly related PRs

Suggested reviewers

  • birdieHyun
  • acceptor-gyu

Poem

🐇🔐 I found a p8 and made a key,
I signed a JWT as quick as can be,
Apple and Kakao now share the gate,
Sessions hop along — users celebrate,
I nibble logs and wink: "Deployed, hurray!"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 'feat: apple login' is directly related to the main change—comprehensive addition of Apple OAuth authentication support throughout the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/apple-login

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: 5

Caution

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

⚠️ Outside diff range comments (1)
src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt (1)

27-52: Misleading parameter name: redirectUri is being used as the token endpoint URL.

The parameter redirectUri on line 49 is used as the POST target for token exchange, but redirectUri typically refers to where the OAuth provider redirects after authorization. This is semantically confusing and likely incorrect.

Consider renaming to tokenUri or passing a separate token endpoint:

-private fun getOauthTokenInternal(code: String, clientId: String, clientSecret: String, redirectUri: String): OauthToken? {
+private fun getOauthTokenInternal(code: String, clientId: String, clientSecret: String, tokenUri: String): OauthToken? {
     // ...
-    val response = restTemplate.postForEntity(redirectUri, request, OauthToken::class.java)
+    val response = restTemplate.postForEntity(tokenUri, request, OauthToken::class.java)

Also, Apple's token endpoint (https://appleid.apple.com/auth/token) requires redirect_uri in the request body, which is currently missing from requestPayloads.

🧹 Nitpick comments (8)
src/main/kotlin/upbrella/be/user/entity/User.kt (1)

29-59: Consider extracting common user creation logic.

The createNewUser and createNewAppleUser methods share nearly identical logic with only the social ID extraction and provider value differing. Consider refactoring to reduce duplication.

Example refactor:

 companion object {
+    private fun createUser(
+        socialId: Long,
+        email: String,
+        provider: String,
+        joinRequest: JoinRequest,
+        aesEncryptor: AesEncryptor
+    ): User {
+        return User(
+            socialId = socialId,
+            name = joinRequest.name,
+            phoneNumber = joinRequest.phoneNumber,
+            email = email,
+            provider = provider,
+            bank = aesEncryptor.encrypt(joinRequest.bank),
+            accountNumber = aesEncryptor.encrypt(joinRequest.accountNumber)
+        )
+    }
+
     fun createNewUser(
         kakaoUser: KakaoLoginResponse,
         joinRequest: JoinRequest,
         aesEncryptor: AesEncryptor
     ): User {
-        return User(
-            socialId = kakaoUser.id.hashCode().toLong(),
-            name = joinRequest.name,
-            phoneNumber = joinRequest.phoneNumber,
-            email = kakaoUser.kakaoAccount?.email ?: "",
-            provider = "KAKAO",
-            bank = aesEncryptor.encrypt(joinRequest.bank),
-            accountNumber = aesEncryptor.encrypt(joinRequest.accountNumber)
-        )
+        return createUser(
+            socialId = kakaoUser.id.hashCode().toLong(),
+            email = kakaoUser.kakaoAccount?.email ?: "",
+            provider = "KAKAO",
+            joinRequest = joinRequest,
+            aesEncryptor = aesEncryptor
+        )
     }
 
     fun createNewAppleUser(
         appleUser: AppleLoginResponse,
         joinRequest: JoinRequest,
         aesEncryptor: AesEncryptor
     ): User {
-        return User(
-            socialId = appleUser.sub.hashCode().toLong(),
-            name = joinRequest.name,
-            phoneNumber = joinRequest.phoneNumber,
-            email = appleUser.email ?: "",
-            provider = "APPLE",
-            bank = aesEncryptor.encrypt(joinRequest.bank),
-            accountNumber = aesEncryptor.encrypt(joinRequest.accountNumber)
-        )
+        return createUser(
+            socialId = appleUser.sub.hashCode().toLong(),
+            email = appleUser.email ?: "",
+            provider = "APPLE",
+            joinRequest = joinRequest,
+            aesEncryptor = aesEncryptor
+        )
     }
 }
src/main/kotlin/upbrella/be/user/service/UserService.kt (1)

64-80: Consider extracting common validation logic to reduce duplication.

The joinApple method duplicates the validation logic (existing user check, blacklist check) from join. While functional, this could be consolidated into a private helper method to improve maintainability.

private fun validateNewUser(socialIdHash: Long) {
    if (userReader.existsBySocialId(socialIdHash)) {
        throw ExistingMemberException("[ERROR] 이미 가입된 회원입니다. 로그인 폼으로 이동합니다.")
    }
    if (blackListReader.existsBySocialId(socialIdHash)) {
        throw BlackListUserException("[ERROR] 정지된 회원입니다. 정지된 회원은 재가입이 불가능합니다.")
    }
}
src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt (1)

8-9: Consider using a regular class or marking as final if CGLIB proxying isn't needed.

The open modifier is unusual in Kotlin Spring components. If this is required for Spring's CGLIB proxying (e.g., for @Transactional), consider adding a comment explaining why. Otherwise, it could be removed.

src/test/kotlin/upbrella/be/user/service/UserServiceDynamicTest.kt (1)

38-48: LGTM - Test fixture updated correctly. Consider adding Apple login/join tests.

The User construction correctly uses the new entity signature with the provider field. The existing Kakao tests continue to work.

Consider adding similar dynamic tests for loginApple and joinApple methods to ensure Apple OAuth flows are covered.

src/main/kotlin/upbrella/be/user/controller/UserController.kt (2)

123-135: Unreachable else branch - consider simplifying.

The else branch on line 134 is unreachable because the condition kakaoUser == null && appleUser == null is already checked on line 119. The when expression can be simplified:

         val loggedInUser = when {
             kakaoUser != null -> {
                 val user = userService.login(kakaoUser.id!!)
                 session.removeAttribute("kakaoUser")
                 user
             }
-            appleUser != null -> {
+            else -> {
+                // appleUser is guaranteed non-null here
                 val user = userService.loginApple(appleUser.sub!!)
                 session.removeAttribute("appleUser")
                 user
             }
-            else -> throw NotSocialLoginedException("[ERROR] 소셜 로그인을 먼저 해주세요.")
         }

176-188: Same unreachable else branch pattern in join flow.

The else branch on line 187 is also unreachable for the same reason. Consider applying the same simplification as suggested for the login flow.

src/main/kotlin/upbrella/be/util/AppleJwtGenerator.kt (2)

26-47: Check JJWT signWith signature vs library version; consider minor hardening of token construction

Functionally this looks correct for Apple client secret generation (claims/headers/audience/ES256 are aligned), but there are a couple of details worth verifying/tuning:

  • The call .signWith(privateKey, SignatureAlgorithm.ES256) matches the newer JJWT API (0.10.x/0.11.x). If your project is still on an older 0.9.x JJWT, the expected signature is signWith(SignatureAlgorithm, Key) and this line will not compile. Please confirm the version and adjust if needed:
-            .signWith(privateKey, SignatureAlgorithm.ES256)
+            // For JJWT 0.10+ (new API)
+            .signWith(privateKey, SignatureAlgorithm.ES256)
+            // For JJWT 0.9.x (old API), you would instead use:
+            // .signWith(SignatureAlgorithm.ES256, privateKey)
  • Minor: the magic number 15777000000L is correct for “~6 months” but opaque. Extracting it to a named constant (e.g. PRIVATE_KEY_MAX_AGE_MS) or using a Duration calculation (Duration.ofDays(180).toMillis()) would make intent clearer, especially for future maintainers.

  • Operationally, if generateClientSecret is called often, consider caching the generated secret until expirationTime instead of regenerating each time, to avoid repeated disk I/O and parsing. (From the broader PR context, it looks like you may already only call this at startup, in which case you’re fine.)


52-61: Make loadPrivateKey more robust and ensure resources are closed via use

Current implementation assumes PEMParser.readObject() always returns PrivateKeyInfo and manually closes the parser. Two small improvements:

  1. Guard against unexpected PEM contents to avoid an unhelpful ClassCastException.
  2. Wrap PEMParser in use so it’s always closed, even if parsing fails.

For example:

-    private fun loadPrivateKey(p8KeyPath: String): PrivateKey {
-        val resource = ClassPathResource(p8KeyPath)
-        val p8Content = resource.inputStream.bufferedReader().use { it.readText() }
-
-        val pemParser = PEMParser(StringReader(p8Content))
-        val privateKeyInfo = pemParser.readObject() as PrivateKeyInfo
-        pemParser.close()
-
-        return JcaPEMKeyConverter().getPrivateKey(privateKeyInfo)
-    }
+    private fun loadPrivateKey(p8KeyPath: String): PrivateKey {
+        val resource = ClassPathResource(p8KeyPath)
+        val p8Content = resource.inputStream.bufferedReader().use { it.readText() }
+
+        return PEMParser(StringReader(p8Content)).use { pemParser ->
+            val obj = pemParser.readObject()
+            val privateKeyInfo = obj as? PrivateKeyInfo
+                ?: throw IllegalArgumentException(
+                    "Unsupported key format in '$p8KeyPath': ${obj?.javaClass?.name ?: "null"}"
+                )
+
+            JcaPEMKeyConverter().getPrivateKey(privateKeyInfo)
+        }
+    }

Also, note that the KDoc and ClassPathResource encourage placing the .p8 key on the application classpath. For production, it’s generally safer to load this from an external secret store or mounted file path (env‑driven configuration) rather than bundling the private key into the application artifact.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 231f159 and 687b680.

📒 Files selected for processing (16)
  • build.gradle (1 hunks)
  • src/main/kotlin/upbrella/be/user/controller/UserController.kt (5 hunks)
  • src/main/kotlin/upbrella/be/user/dto/response/AppleLoginResponse.kt (1 hunks)
  • src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt (1 hunks)
  • src/main/kotlin/upbrella/be/user/entity/User.kt (3 hunks)
  • src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt (4 hunks)
  • src/main/kotlin/upbrella/be/user/service/UserService.kt (3 hunks)
  • src/main/kotlin/upbrella/be/util/AppleJwtGenerator.kt (1 hunks)
  • src/test/kotlin/upbrella/be/rent/controller/RentControllerTest.kt (2 hunks)
  • src/test/kotlin/upbrella/be/rent/entity/HistoryTest.kt (1 hunks)
  • src/test/kotlin/upbrella/be/rent/service/ConditionReportServiceTest.kt (1 hunks)
  • src/test/kotlin/upbrella/be/rent/service/RentServiceTest.kt (1 hunks)
  • src/test/kotlin/upbrella/be/slack/service/SlackAlarmServiceTest.kt (1 hunks)
  • src/test/kotlin/upbrella/be/user/controller/UserControllerTest.kt (3 hunks)
  • src/test/kotlin/upbrella/be/user/integration/UserIntegrationTest.kt (3 hunks)
  • src/test/kotlin/upbrella/be/user/service/UserServiceDynamicTest.kt (1 hunks)
🧰 Additional context used
🪛 detekt (1.23.8)
src/main/kotlin/upbrella/be/user/controller/UserController.kt

[warning] 97-97: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (11)
src/test/kotlin/upbrella/be/slack/service/SlackAlarmServiceTest.kt (1)

72-82: LGTM!

The User construction has been properly updated to use named parameters including the new provider field.

src/test/kotlin/upbrella/be/rent/service/ConditionReportServiceTest.kt (1)

62-72: LGTM!

The User construction has been properly updated to use named parameters including the new provider field.

src/main/kotlin/upbrella/be/user/entity/User.kt (1)

19-19: LGTM!

The provider field properly distinguishes between KAKAO and APPLE login types with a sensible default.

src/test/kotlin/upbrella/be/rent/entity/HistoryTest.kt (1)

49-59: LGTM!

The User construction has been properly updated to use named parameters including the new provider field.

src/test/kotlin/upbrella/be/rent/controller/RentControllerTest.kt (2)

135-145: LGTM!

The User construction has been properly updated to use named parameters including the new provider field.


220-231: LGTM!

The User construction has been properly updated to use named parameters including the new provider field.

src/test/kotlin/upbrella/be/user/controller/UserControllerTest.kt (1)

56-56: LGTM!

The AppleOauthInfo has been properly added to the test setup, mirroring the existing KakaoOauthInfo pattern.

Also applies to: 70-70

src/test/kotlin/upbrella/be/user/integration/UserIntegrationTest.kt (1)

59-60: LGTM!

The AppleOauthInfo has been properly autowired and integrated into the controller setup for integration testing.

Also applies to: 85-85

src/main/kotlin/upbrella/be/user/service/UserService.kt (1)

40-44: LGTM - loginApple follows the established pattern.

The implementation correctly mirrors the Kakao login flow, using the Apple sub identifier hashed to a Long for user lookup.

src/test/kotlin/upbrella/be/rent/service/RentServiceTest.kt (1)

120-130: LGTM - Test fixture correctly updated for new User entity.

The User construction now uses named parameters and includes the new provider field, properly reflecting the entity changes for Apple OAuth support.

src/main/kotlin/upbrella/be/user/controller/UserController.kt (1)

91-112: Apple OAuth endpoint structure looks good, but verify the OAuth flow implementation.

The endpoint structure correctly mirrors the Kakao flow. However, as noted in the OauthLoginService review, the Apple OAuth implementation may not work correctly because Apple doesn't have a user info endpoint. The id_token from the token response should be decoded directly instead of making a separate API call.

Comment on lines +91 to +95
// JWT for Apple Sign In
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
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

🏁 Script executed:

#!/bin/bash
# Check for security advisories using GitHub CLI for Maven packages
# and search for current version information

echo "=== Checking for JJWT (io.jsonwebtoken) CVEs ==="
# Using GitHub API to check advisories - note: may need alternative approach if API limits hit
curl -s "https://api.github.com/repos/jwtk/jjwt/issues?state=all&labels=security" | head -20

echo -e "\n=== Checking for BouncyCastle CVEs ==="
curl -s "https://api.github.com/repos/bcgit/bc-java/issues?state=all&labels=security" | head -20

echo -e "\n=== Checking Maven Central for JJWT latest version ==="
curl -s "https://repo1.maven.org/maven2/io/jsonwebtoken/jjwt-api/maven-metadata.xml" | grep -oP '<release>\K[^<]+' | tail -1

echo -e "\n=== Checking Maven Central for BouncyCastle latest version ==="
curl -s "https://repo1.maven.org/maven2/org/bouncycastle/bcpkix-jdk15on/maven-metadata.xml" | grep -oP '<release>\K[^<]+' | tail -1

Repository: UPbrella/UPbrella_back

Length of output: 296


🌐 Web query:

JJWT library io.jsonwebtoken jjwt-api version 0.11.5 security vulnerabilities CVE

💡 Result:

Short answer — yes, there are public security advisories affecting JJWT variants around version 0.11.5:

  • CVE-2024-31033 (Weak/incorrect key handling in JJWT’s setSigningKey()/signWith()). This CVE/GHSA describes JJWT ignoring certain characters which can make keys appear stronger than they are; it is reported against jjwt-impl up through 0.12.5 (vendor disputes/notes). See NVD/Snyk/other advisories. [1][2][3]

  • Additional CVEs shown on package pages for jjwt artifacts (0.11.5) are usually from transitive dependencies (notably Jackson-related deserialization and other library CVEs). Maven/MvnRepository listings for jjwt 0.11.5 show dependency CVEs (examples vary by artifact: jjwt-jackson, jjwt-orgjson, jjwt-root). Check the specific artifact(s) you use to see which dependency CVEs apply. [4][5]

What to do

  • If you use jjwt-impl (or any jjwt artifact), treat CVE-2024-31033 as relevant and plan to upgrade to a fixed/newer version (project recommends moving to newer 0.12.x/0.13.x lines or follow maintainer guidance). Verify exact affected artifact (jjwt-impl vs jjwt-api) in your dependency tree. [2][4]

References

  1. NVD / CVE-2024-31033 entry. [NVD]. [turn1search4].
  2. Snyk advisory for io.jsonwebtoken:jjwt-impl / CVE-2024-31033. [turn1search8].
  3. GitHub issue on jwtk/jjwt discussing the CVE report. [turn0search0].
  4. mvnrepository pages for jjwt artifacts (shows dependency CVEs for 0.11.5). [turn0search7][turn0search1].

If you want, I can (a) run a dependency-tree scan of your build file (pom/gradle) to list which jjwt artifact(s) and transitive CVEs affect you, or (b) recommend exact upgrade versions and mitigation steps for your setup.


🌐 Web query:

BouncyCastle bcpkix-jdk15on version 1.70 security vulnerabilities CVE 2021 2022 2023 2024

💡 Result:

Short answer — yes: bcpkix-jdk15on 1.70 is affected by multiple publicly disclosed Bouncy Castle CVEs from 2023–2024. Key items:

  • CVE-2023-33201 (LDAP injection — affects Bouncy Castle Java < 1.74). 1.70 is vulnerable. [1][2]
  • CVE-2023-33202 (PEMParser crafted ASN.1 → OutOfMemoryError DoS — affects Bouncy Castle Java < 1.73). 1.70 is vulnerable. [3][4]
  • CVE-2024-29857 (ECCurve/F2m certificate parsing → excessive CPU / DoS — affects BC Java < 1.78). 1.70 is vulnerable. [5][6]
  • CVE-2024-34447 (BCJSSE hostname verification / DNS poisoning — affects BC Java < 1.78). 1.70 is vulnerable. [7][8]

Notes:

  • CVE-2022-45146 targets the BC‑FJA (FIPS Java API) modules (BC‑FJA) and not bcpkix-jdk15on; it does not apply to plain bcpkix builds. [9]
  • Older CVEs (e.g., timing issue CVE-2020-15522 / GHSA-2021) affect versions < 1.66 and therefore do not affect 1.70. [10]

References

  1. GitHub advisory / CVE-2023-33201 (Bouncy Castle LDAP injection).
  2. NVD / CVE-2023-33201 summaries (vendor/GitHub pages).
  3. GitHub advisory / CVE-2023-33202 (PEMParser OOM DoS).
  4. GitLab / dependency-scanning entries for bcpkix-jdk15on (CVE-2023-33202).
  5. NVD / CVE-2024-29857 and Bouncy Castle advisory (ECCurve high CPU).
  6. GitHub Advisory Database / GHSA-8xfc-gm6g-vgpv (CVE-2024-29857).
  7. NVD / CVE-2024-34447 (BC TLS / hostname verification DNS issue).
  8. GitHub Advisory Database / GHSA-4h8f-2wvx-gg5w (CVE-2024-34447).
  9. CVE-2022-45146 (BC‑FJA FIPS Java API) — CVE details / cvedetails.
  10. GHSA/GitHub advisory for timing attack (CVE-2020-15522) showing affected versions <1.66.

If you want, I can: (a) produce a one-line remediation plan (recommended upgrade versions), or (b) fetch the exact vendor advisory pages/patch commits and provide direct links. Which would you prefer?


🌐 Web query:

latest stable versions io.jsonwebtoken jjwt and org.bouncycastle bcpkix 2025

💡 Result:

  • io.jsonwebtoken (JJWT): latest stable release 0.13.0 — released Aug 20, 2025. [1]

  • org.bouncycastle (BCP KIX): latest stable bcpkix (bc-java) release 1.82 — released Sep 17, 2025. (There is a 1.83-SNAPSHOT beta available.) [2][3]

Sources:
[1] Maven / JJWT 0.13.0 release details.
[2] Bouncy Castle Java releases — 1.82 release notes (Sep 17, 2025).
[3] Bouncy Castle betas / 1.83-SNAPSHOT listing.


Update dependency versions to address multiple confirmed security vulnerabilities.

Both libraries in lines 91-95 contain publicly disclosed CVEs that require immediate attention:

JJWT 0.11.5:

  • CVE-2024-31033: Weak/incorrect key handling in jjwt-impl affecting version 0.11.5 (fixed in 0.12.x/0.13.x)
  • Upgrade to: 0.13.0 (latest stable, Aug 2025)

BouncyCastle 1.70:

Update the snippet to:

    // JWT for Apple Sign In
    implementation 'io.jsonwebtoken:jjwt-api:0.13.0'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.13.0'
    implementation 'org.bouncycastle:bcpkix-jdk15on:1.82'
🤖 Prompt for AI Agents
In build.gradle around lines 91 to 95 the project pins vulnerable versions of
JJWT and BouncyCastle; update the dependency coordinates to the secure versions
recommended in the review: change io.jsonwebtoken:jjwt-api, jjwt-impl and
jjwt-jackson to 0.13.0 and org.bouncycastle:bcpkix-jdk15on to 1.82, then refresh
Gradle dependencies (./gradlew --refresh-dependencies), rebuild and run your
test suite to ensure no API breakages; if any compile errors occur, adjust
imports or code to match the 0.13.0 jjwt API or resolve BouncyCastle API changes
accordingly.

Comment on lines 95 to 99
try {
appleAccessToken = oauthLoginService.getOauthToken(code.code, appleOauthInfo)!!
} catch (e: HttpClientErrorException) {
throw InvalidLoginCodeException("[ERROR] 로그인 코드가 유효하지 않습니다.")
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Exception is swallowed - original exception context is lost.

Per static analysis: the HttpClientErrorException is caught and replaced with InvalidLoginCodeException, losing the original error details which could be valuable for debugging. Consider including the original exception as the cause:

         try {
             appleAccessToken = oauthLoginService.getOauthToken(code.code, appleOauthInfo)!!
         } catch (e: HttpClientErrorException) {
-            throw InvalidLoginCodeException("[ERROR] 로그인 코드가 유효하지 않습니다.")
+            throw InvalidLoginCodeException("[ERROR] 로그인 코드가 유효하지 않습니다.", e)
         }

This requires InvalidLoginCodeException to accept a cause parameter. The same issue exists in kakaoLogin (lines 74-76).

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 97-97: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/user/controller/UserController.kt around lines
74-76 and 95-99, the catch blocks for HttpClientErrorException discard the
original exception which loses debugging context; update the catch to rethrow
InvalidLoginCodeException including the caught exception as its cause (e.g.,
pass the caught exception into the InvalidLoginCodeException constructor) and,
if necessary, modify InvalidLoginCodeException to provide a constructor that
accepts a message and a cause so the original stack trace and message are
preserved.

Comment on lines +5 to +14
data class AppleLoginResponse(
@JsonProperty("sub")
val sub: String?, // Apple user identifier

@JsonProperty("email")
val email: String?,

@JsonProperty("email_verified")
val emailVerified: Boolean?
)
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

Consider making sub non-nullable or adding null-safety checks in callers.

Apple's sub claim is always present in valid ID tokens. Making it nullable here can lead to NPE issues. In UserService.kt, appleUser.sub.hashCode() is called without null checks (lines 65 and 130), which will throw NPE if sub is null.

Either make sub non-null:

 data class AppleLoginResponse(
     @JsonProperty("sub")
-    val sub: String?, // Apple user identifier
+    val sub: String, // Apple user identifier (always present)

Or ensure callers validate before use:

val socialIdHash = appleUser.sub?.hashCode()?.toLong()
    ?: throw IllegalArgumentException("Apple sub is required")
🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/user/dto/response/AppleLoginResponse.kt around
lines 5 to 14, the AppleLoginResponse.sub field is declared nullable but callers
in UserService.kt call sub.hashCode() without null checks; make sub non-nullable
(remove nullable marker) so the type reflects Apple's guarantee and forces
callers to treat it as required, or alternatively keep it nullable and update
UserService to defensively check and throw a clear exception or handle the
missing value before calling hashCode(); ensure all callsites (lines ~65 and
~130 in UserService.kt) are updated to match the chosen approach.

Comment on lines 35 to 44
@PostConstruct
fun init() {
// Apple JWT Client Secret을 동적으로 생성
_clientSecret = appleJwtGenerator.generateClientSecret(
teamId = teamId,
keyId = keyId,
clientId = clientId,
p8KeyPath = p8KeyPath
)
}
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:

Apple Sign In client secret expiration maximum validity duration

💡 Result:

Maximum validity is 6 months (must not exceed 15,777,000 seconds from iat). [1][2]

Sources:
[1] Apple — Generate and validate tokens (Sign in with Apple).
[2] Pomerium docs — Apple integration (quoting Apple's exp limit).


🌐 Web query:

javax.annotation.PostConstruct deprecated Java version

💡 Result:

javax.annotation.PostConstruct (part of the JSR‑250 "common annotations" in module java.xml.ws.annotation) was deprecated for removal in Java 9 and the Java EE / common‑annotations modules were removed in JDK 11 — so PostConstruct is no longer provided by the JDK starting with Java 11. Add the javax.annotation-api (or jakarta.annotation-api) dependency to restore it. [1][2][3]

Sources:

  • Oracle JDK removed Java EE / Common Annotations in JDK 11. [1]
  • OpenJDK JIRA describing deprecation for removal in Java 9. [2]
  • Practical guidance to add javax.annotation-api when using Java 11. [3]

🏁 Script executed:

# First, let's examine the full AppleOauthInfo.kt file to understand the implementation
fd AppleOauthInfo.kt

Repository: UPbrella/UPbrella_back

Length of output: 126


🏁 Script executed:

cat -n src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt

Repository: UPbrella/UPbrella_back

Length of output: 1500


🏁 Script executed:

# Search for any secret rotation, refresh, or regeneration logic
rg -i "regenerate|rotation|refresh|expir" src/main/kotlin/upbrella/be/user/dto/token/

Repository: UPbrella/UPbrella_back

Length of output: 242


🏁 Script executed:

# Check the project's Java/Kotlin version configuration
fd -e properties -e gradle -e maven -e toml | head -20

Repository: UPbrella/UPbrella_back

Length of output: 135


🏁 Script executed:

cat -n build.gradle | head -100

Repository: UPbrella/UPbrella_back

Length of output: 4018


🏁 Script executed:

# Find and examine AppleJwtGenerator
fd AppleJwtGenerator.kt

Repository: UPbrella/UPbrella_back

Length of output: 119


🏁 Script executed:

# Search for scheduled tasks, timers, or refresh mechanisms
rg -i "scheduled|timer|interval|refreshtoken" --type=kt | head -20

Repository: UPbrella/UPbrella_back

Length of output: 92


🏁 Script executed:

cat -n src/main/kotlin/upbrella/be/util/AppleJwtGenerator.kt

Repository: UPbrella/UPbrella_back

Length of output: 2444


🏁 Script executed:

# Search for scheduled tasks with correct syntax
rg -i "scheduled|@scheduled" --type=kotlin

Repository: UPbrella/UPbrella_back

Length of output: 48


🏁 Script executed:

# Verify if spring-boot-starter transitively provides javax.annotation-api in Spring Boot 2.7.13
cat -n build.gradle | grep -A 2 "spring-boot-starter"

Repository: UPbrella/UPbrella_back

Length of output: 826


Apple client secrets expire after 6 months — implement regeneration before expiration.

The Apple JWT client secret is generated at application startup with a 6-month expiration (line 33 in AppleJwtGenerator.kt: Date(now.time + 15777000000L)). However, AppleOauthInfo generates this secret only once via @PostConstruct (lines 35-44), with no mechanism to regenerate it before expiration. For long-running applications, this will cause authentication failures once the token expires.

Implement one of:

  1. Schedule periodic secret regeneration (e.g., every 5 months via @Scheduled)
  2. Generate secrets on-demand per request with caching
  3. Ensure application restarts before secret expiration

Additionally, upgrade from the deprecated javax.annotation.PostConstruct to jakarta.annotation.PostConstruct (add jakarta.annotation:jakarta.annotation-api dependency) or use Spring's @EventListener(ApplicationReadyEvent::class) for Java 17+ compatibility.

🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt around lines 35
to 44, the client secret is generated only once in a @PostConstruct method which
will expire after 6 months; replace the one-time init with a regeneration
strategy and update the lifecycle annotation: (1) switch from
javax.annotation.PostConstruct to jakarta.annotation.PostConstruct or use
Spring's @EventListener(ApplicationReadyEvent::class) to avoid deprecated javax,
and (2) implement periodic regeneration by adding a scheduled task (e.g.,
@Scheduled with a fixedRate or cron to run every ~5 months) that re-calls
appleJwtGenerator.generateClientSecret(...) and updates _clientSecret
atomically, or alternatively implement on-demand lazy generation with a cached
value and expiry check that regenerates before returning if expired; ensure
thread-safety when updating the shared secret and add necessary dependency
jakarta.annotation:jakarta.annotation-api if choosing jakarta.

Comment on lines +1 to +6
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgPGJKUUO/Pk7S7u5c
VLfvBLGWRfHR3Ja6mZjKJg8Q0c2gCgYIKoZIzj0DAQehRANCAARLYCMOVGDALwBK
bKD6QdqFv8i5dX3LVxHiCcBPdlQPqFHpkOLQPF8eHvKuqTmVNjjZ9L5q6pDKxIUH
Qr0qJKqC
-----END PRIVATE KEY-----
Copy link
Member

Choose a reason for hiding this comment

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

private 인데 공개해도 괜찮나여?

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: 2

Caution

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

⚠️ Outside diff range comments (1)
src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt (1)

29-54: Fix Kakao OAuth token endpoint configuration—redirectUri should not be used as the POST URL.

The current implementation incorrectly uses redirectUri as the token endpoint (line 51), when it should be the OAuth callback URL. This violates the OAuth 2.0 specification.

Compare with Apple's correct implementation (lines 73, 80):

  • Token endpoint is hard-coded: https://appleid.apple.com/auth/token
  • redirect_uri is passed as a request parameter

Kakao should follow the same pattern:

  1. Add a tokenUri configuration property (e.g., https://kauth.kakao.com/oauth/token)
  2. Add "redirect_uri" to redirectUri to the request payload (line 45, like Apple)
  3. Update line 51 to use the token endpoint URL, not redirectUri

The test value redirectUri = "kakao.com" (line 49 in OauthLoginServiceTest.kt) further confirms this is incorrect—it's neither a valid token endpoint nor a proper callback URL.

♻️ Duplicate comments (2)
src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt (2)

7-7: Use jakarta.annotation.PostConstruct instead of deprecated javax.annotation.PostConstruct.

This annotation was deprecated in Java 9 and removed in Java 11. Since the project uses Java 17, add the jakarta.annotation:jakarta.annotation-api dependency and update the import.

-import javax.annotation.PostConstruct
+import jakarta.annotation.PostConstruct

44-52: Client secret expires after 6 months with no regeneration mechanism.

The Apple JWT client secret is generated once at startup. For long-running applications, this will cause authentication failures after 6 months. Consider implementing periodic regeneration via @Scheduled or lazy generation with expiry checking.

🧹 Nitpick comments (7)
src/main/kotlin/upbrella/be/config/ProductionCorsConfig.kt (1)

15-22: Re‑check need to allow https://appleid.apple.com as a CORS origin with credentials

Because allowCredentials(true) is enabled, adding https://appleid.apple.com means any JS running on that domain can call your API with user cookies. If the backend never receives browser XHR/fetch calls directly from Apple (only redirects and server‑to‑server token calls), you can safely drop this origin to keep the CORS surface minimal.

src/main/kotlin/upbrella/be/user/controller/UserController.kt (2)

134-153: Tighten null‑safety and session cleanup in multi‑provider login

The combined Kakao/Apple login flow is mostly straightforward, but a couple of small robustness points:

  • You dereference kakaoUser.id!! and appleUser.sub!!. If the upstream DTOs allow these fields to be null, this will 500. Either make those fields non‑nullable in the DTOs or add an explicit null check with a controlled error.
  • When both kakaoUser and appleUser are present (edge case), the when prefers Kakao and leaves the Apple session attribute untouched. Consider clearing both social attributes after a successful login to avoid stale data hanging around in the session.

183-207: Broadened join flow is good; consider renaming and symmetrical cleanup

Extending /users/join to handle both Kakao and Apple, and updating the message to "소셜 회원가입 성공", makes the behavior clearer. Now that it’s no longer Kakao‑specific:

  • The method name kakaoJoin is slightly misleading; consider renaming to something like socialJoin for clarity.
  • Similar to login, you might want to defensively clear both "kakaoUser" and "appleUser" session attributes after a successful join, regardless of which provider was used, to prevent leftover social state.

Also applies to: 216-216

src/main/kotlin/upbrella/be/util/AppleIdTokenDecoder.kt (3)

73-80: Consider caching Apple public keys to reduce latency.

Fetching public keys from Apple's JWKS endpoint on every token decode adds unnecessary latency. Apple's keys rotate infrequently (typically months apart).

Consider caching the keys with a TTL (e.g., 24 hours) or implementing a cache-invalidation strategy that refetches keys only when a kid is not found in the cache.


82-86: Add defensive validation for malformed tokens.

If idToken doesn't contain a . separator, split(".")[0] won't fail but split(".") on a completely invalid string could produce unexpected results. Consider adding a format check.

 private fun parseHeader(idToken: String): Map<String, Any> {
+    val parts = idToken.split(".")
+    if (parts.size != 3) {
+        throw IllegalArgumentException("Invalid JWT format")
+    }
-    val headerEncoded = idToken.split(".")[0]
+    val headerEncoded = parts[0]
     val headerDecoded = String(Base64.getUrlDecoder().decode(headerEncoded))
     return objectMapper.readValue(headerDecoded, Map::class.java) as Map<String, Any>
 }

39-43: Handle email_verified as String or Boolean.

Apple may return email_verified as a String "true"/"false" in some cases rather than a Boolean. The current safe cast as? Boolean would return null if it's a String.

         return AppleLoginResponse(
             sub = claims["sub"] as? String,
             email = claims["email"] as? String,
-            emailVerified = claims["email_verified"] as? Boolean
+            emailVerified = when (val ev = claims["email_verified"]) {
+                is Boolean -> ev
+                is String -> ev.toBoolean()
+                else -> null
+            }
         )
src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt (1)

16-92: Consider reducing duplication between Dev and Prod implementations.

DevAppleOauthInfo and ProdAppleOauthInfo are nearly identical except for property key suffixes. Consider consolidating to a single implementation using profile-specific property files (e.g., application-dev.yml and application-prod.yml) with common property names.

@Component
class AppleOauthInfoImpl(
    @Value("\${apple.client-id}")
    override val clientId: String,
    @Value("\${apple.team-id}")
    private val teamId: String,
    // ... other properties with common names
    private val appleJwtGenerator: AppleJwtGenerator
) : AppleOauthInfo {
    // single implementation
}

Then define profile-specific values in application-dev.yml:

apple:
  client-id: ${APPLE_CLIENT_ID_DEV}
  team-id: ${APPLE_TEAM_ID_DEV}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f6db83 and 6825b43.

📒 Files selected for processing (11)
  • src/main/kotlin/upbrella/be/config/AuthConfig.kt (1 hunks)
  • src/main/kotlin/upbrella/be/config/ProductionCorsConfig.kt (1 hunks)
  • src/main/kotlin/upbrella/be/config/SessionCookieConfig.kt (1 hunks)
  • src/main/kotlin/upbrella/be/user/controller/UserController.kt (5 hunks)
  • src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt (1 hunks)
  • src/main/kotlin/upbrella/be/user/dto/token/OauthToken.kt (1 hunks)
  • src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt (3 hunks)
  • src/main/kotlin/upbrella/be/util/AppleIdTokenDecoder.kt (1 hunks)
  • src/main/resources/application-dev.yml (2 hunks)
  • src/main/resources/application-prod.yml (1 hunks)
  • src/test/kotlin/upbrella/be/user/service/OauthLoginServiceTest.kt (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (9)
src/main/kotlin/upbrella/be/config/AuthConfig.kt (1)

20-35: Apple callback exclusion from LoginInterceptor looks appropriate

Excluding /auth/apple/** from loginInterceptor so the OAuth callback can be reached without a prior login is consistent with the existing Kakao login exclusions.

src/main/resources/application-dev.yml (1)

5-7: Env‑driven DB config and dev cookie settings look reasonable

Switching dev DB config to ${DATABASE_URL}/${DATABASE_USERNAME}/${DATABASE_PASSWORD} is a good move away from hard‑coded secrets. The dev session cookie configuration (same-site: lax, secure: false) is also appropriate for local/non‑TLS dev, just be aware behavior will differ slightly from prod for cross‑site OAuth flows.

Also applies to: 19-24

src/main/kotlin/upbrella/be/config/SessionCookieConfig.kt (1)

1-23: Prod session cookie hardening is good; verify domain & config consistency

Setting secure = true, httpOnly = true, and path = "/" under prod is solid. Please double‑check:

  • domain = "upbrella.co.kr" matches your actual public base domain for all API/front‑end hosts that must share the session (e.g. api.upbrella.co.kr, www.upbrella.co.kr).
  • This programmatic config stays consistent with server.servlet.session.cookie.* properties in application-prod.yml (notably same-site: none).
src/main/resources/application-prod.yml (1)

20-24: SameSite=None matches cross‑site OAuth needs; keep it aligned with secure

Setting server.servlet.session.cookie.same-site: none is appropriate for cross‑site flows like Apple OAuth. Since SessionCookieConfig sets secure = true, this satisfies modern browser requirements for SameSite=None cookies; just ensure secure is never disabled in prod, or cookies may be rejected.

src/main/kotlin/upbrella/be/user/dto/token/OauthToken.kt (1)

11-13: Adding idToken to OauthToken is backwards‑compatible and aligns with Apple

Introducing val idToken: String? = null with a default keeps existing positional constructor usages valid and, with the snake_case strategy, correctly binds to the id_token field returned by providers like Apple.

src/test/kotlin/upbrella/be/user/service/OauthLoginServiceTest.kt (1)

28-30: AppleIdTokenDecoder mock wiring is correct

Adding the appleIdTokenDecoder mock keeps @InjectMocks OauthLoginService constructible with the new dependency. Once you add Apple‑specific service tests, you can reuse this mock for those scenarios.

src/main/kotlin/upbrella/be/user/controller/UserController.kt (1)

7-14: Apple OAuth wiring into UserController looks consistent

Importing RedirectView and AppleOauthInfo, and injecting AppleOauthInfo alongside KakaoOauthInfo, aligns with the new Apple OAuth flow used in the controller and services. As long as you have matching AppleOauthInfo beans for all active profiles (dev/prod/test), this wiring is sound.

Also applies to: 27-34

src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt (2)

56-83: LGTM!

The Apple token retrieval correctly uses Apple's token endpoint and includes all required parameters including redirect_uri in the request payload.


107-110: LGTM!

Clean delegation to AppleIdTokenDecoder for ID token processing.

Comment on lines +92 to +130
@PostMapping("/auth/apple")
fun appleLoginCallback(
session: HttpSession,
@RequestParam code: String,
@RequestParam(required = false) state: String?,
@RequestParam(required = false) id_token: String?,
@RequestParam(required = false) user: String?
): RedirectView {
log.info("Apple login callback received - code: ${code.take(10)}...")

val appleOauthToken: OauthToken

try {
appleOauthToken = oauthLoginService.getOauthToken(code, appleOauthInfo)!!
} catch (e: HttpClientErrorException) {
log.error("Apple login failed", e)
return RedirectView("https://upbrella.co.kr/login?error=apple_login_failed")
} catch (e: Exception) {
log.error("Unexpected error during Apple login", e)
return RedirectView("https://upbrella.co.kr/login?error=server_error")
}

// id_token이 없으면 에러
if (appleOauthToken.idToken.isNullOrEmpty()) {
log.error("Apple ID token is null or empty")
return RedirectView("https://upbrella.co.kr/login?error=no_id_token")
}

try {
val appleLoggedInUser = oauthLoginService.processAppleLogin(appleOauthToken.idToken!!)
session.setAttribute("appleUser", appleLoggedInUser)

log.info("Apple social login success - redirecting to frontend")
return RedirectView("https://upbrella.co.kr/login?apple=success")
} catch (e: Exception) {
log.error("Apple ID token validation failed", e)
return RedirectView("https://upbrella.co.kr/login?error=token_validation_failed")
}
}
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

I'll verify the three main concerns in this Apple OAuth callback review. Let me start by exploring the codebase to understand the full OAuth flow and check the specific issues raised.
<function_calls>

find . -type f -name "*.kt" | grep -E "(oauth|user|controller)" | head -20

</function_calls>

Let me search more broadly and also check the structure:
<function_calls>

ls -la src/main/kotlin/upbrella/be/user/controller/ 2>/dev/null || find . -type f -name "UserController.kt" | head -5

</function_calls>

I need to run shell scripts to examine the codebase. Let me start the investigation:
<function_calls>

#!/bin/bash

Find UserController and related files

echo "=== Finding UserController ==="
find . -type f -name "UserController.kt" 2>/dev/null

echo ""
echo "=== Finding OAuth-related files ==="
find . -type f -name "OAuth" -o -name "oauth" 2>/dev/null | head -20

echo ""
echo "=== Finding service files ==="
find . -type f -name "*Service.kt" | grep -i oauth 2>/dev/null


</function_calls>

Let me use a more targeted approach:
<function_calls>

#!/bin/bash

Search for the UserController file

rg -l "appleLoginCallback" --type kt 2>/dev/null | head -5


</function_calls>

Let me try a broader search:
<function_calls>

#!/bin/bash

List directory structure

find . -type d -name "user" | head -10
find . -type d -name "controller" | head -10


</function_calls>

Let me search for files containing the code snippet:
<function_calls>

#!/bin/bash
rg "@PostMapping("/auth/apple")" -t kt --max-count 5


</function_calls>

Let me try a different approach and look at git repository structure:
<function_calls>

#!/bin/bash
git ls-files | grep -E "(UserController|oauth)" | head -20


</function_calls>

Now let me search for Apple login related code:
<function_calls>

#!/bin/bash
rg "appleLoginCallback|processAppleLogin|getOauthToken" -t kt -A 2 -B 2 | head -100


</function_calls>

Let me search for state parameter handling:
<function_calls>

#!/bin/bash
rg "state" -t kt -i | grep -E "(session|state|csrf)" | head -30


</function_calls>

Let me search for configuration of redirect URLs:
<function_calls>

#!/bin/bash
rg "upbrella.co.kr|redirectUri|frontendUrl|frontend" -t kt -A 1 -B 1 | head -50


</function_calls>

Let me check for application properties or configuration files:
<function_calls>

#!/bin/bash
find . -type f ( -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" ) | head -10


</function_calls>

Let me search for other OAuth providers to see the pattern:
<function_calls>

#!/bin/bash
rg "@PostMapping.*auth.*callback" -t kt -A 5 | head -100


</function_calls>

Let me check for OauthLoginService implementation:
<function_calls>

#!/bin/bash
rg "class OauthLoginService|fun processAppleLogin|fun getOauthToken" -t kt -A 3 | head -100


</function_calls>

Let me try to get all Kotlin files and examine the structure:
<function_calls>

#!/bin/bash

Get all Kotlin files

find . -type f -name "*.kt" 2>/dev/null | wc -l
echo "---"

Look for controller files

find . -type f -name "*.kt" 2>/dev/null | grep -i controller


</function_calls>

Let me search for the specific error messages from the code:
<function_calls>

#!/bin/bash
rg "Apple login callback received" -t kt -B 5 -A 30


</function_calls>

Let me try searching with the GitHub endpoint:
<function_calls>

#!/bin/bash
rg "Apple ID token is null" -t kt -B 10 -A 20


</function_calls>

Let me search broadly for all file extensions to understand the project better:
<function_calls>

#!/bin/bash
find . -type f -name "*.kt" | head -1 | xargs wc -l


</function_calls>

Let me try listing repository files more directly:
<function_calls>

#!/bin/bash
git ls-files --full-name | grep ".kt$" | head -20


</function_calls>

Now let me examine those files for Apple login logic:
<function_calls>

#!/bin/bash

Search for Apple in all Kotlin files

git ls-files --full-name | grep ".kt$" | xargs rg "apple|Apple" -i -l | head -10


</function_calls>

Let me get the full file content of UserController if it exists:
<function_calls>

#!/bin/bash
git ls-files --full-name | grep -i "usercontroller"


</function_calls>

Let me try a different pattern search:
<function_calls>

#!/bin/bash
git ls-files --full-name | grep ".kt$" | xargs grep -l "appleLoginCallback" 2>/dev/null


</function_calls>

Now let me examine the authentication/OAuth flow more carefully:
<function_calls>

#!/bin/bash

Search for all POST mapping with auth

git ls-files --full-name | grep ".kt$" | xargs grep -n "@PostMapping" 2>/dev/null | grep -i "auth" | head -20


</function_calls>

Let me search for session attribute handling:
<function_calls>

#!/bin/bash

Search for session state or state validation

git ls-files --full-name | grep ".kt$" | xargs grep -n "session.*state|setAttribute.*state|getAttribute.*state" 2>/dev/null | head -20


</function_calls>

Let me look for the Apple OAuth configuration:
<function_calls>

#!/bin/bash

Search for Apple OAuth configuration

git ls-files --full-name | xargs grep -n "apple.*oauth|AppleOauth" 2>/dev/null -i | head -20


</function_calls>

Let me search for where the Apple login is initiated:
<function_calls>

#!/bin/bash

Search for Apple login start or initiate

git ls-files --full-name | grep ".kt$" | xargs grep -n "appleLogin|apple.*login" 2>/dev/null -i | head -30


</function_calls>

Let me try searching for the URL that's hard-coded:
<function_calls>

#!/bin/bash
git ls-files --full-name | xargs grep -n "upbrella.co.kr" 2>/dev/null | head -20


</function_calls>

Let me check if there's configuration for base URL:
<function_calls>

#!/bin/bash

Look for property files

git ls-files --full-name | grep -E ".(yml|yaml|properties)$"


</function_calls>

Let me check the content of those config files:
<function_calls>

#!/bin/bash

Read application config files if they exist

git ls-files --full-name | grep -E "application.*.(yml|yaml|properties)$" | xargs cat 2>/dev/null | head -100


</function_calls>

Let me search for all OAuth-related endpoints:
<function_calls>

#!/bin/bash
git ls-files --full-name | grep ".kt$" | xargs grep -n "@.*Mapping.*oauth|@.*Mapping.*auth" 2>/dev/null -i | head -30


</function_calls>

Let me get a comprehensive list of all controller methods:
<function_calls>

#!/bin/bash
git ls-files --full-name | grep "Controller.kt$" | head -10


</function_calls>

Let me try to find the file using a broader search:
<function_calls>

#!/bin/bash

Search all files containing both "PostMapping" and "/auth/apple"

git ls-files --full-name | xargs grep -l "PostMapping.*apple|apple.*PostMapping" 2>/dev/null


</function_calls>

Let me search more broadly:
<function_calls>

#!/bin/bash

Find all files mentioning "auth/apple"

git ls-files --full-name | xargs grep -l "/auth/apple" 2>/dev/null


</function_calls>

Let me examine the git history or search the PR:
<function_calls>

#!/bin/bash

Check the git diff to see what files changed

git diff --name-only main...HEAD 2>/dev/null | head -20


</function_calls>

Let me try to get the current branch and see what files are in the PR:
<function_calls>

#!/bin/bash
git log --oneline -5


</function_calls>

Let me check the git status and recent changes:
<function_calls>

#!/bin/bash
git status


</function_calls>

Let me try to find files containing the exact method signature:
<function_calls>

#!/bin/bash

Search for the method signature

rg "fun appleLoginCallback" -t kt -B 5 -A 30 2>/dev/null


</function_calls>

Let me try using ast-grep to find this function:
<function_calls>

#!/bin/bash

Try to find the function with ast-grep

ast-grep --pattern 'fun appleLoginCallback($$$) { $$$ }' 2>/dev/null | head -50


</function_calls>

Let me search for all files in the repository:
<function_calls>

#!/bin/bash

List all kotlin files

find . -name "*.kt" -type f 2>/dev/null | head -50


</function_calls>

Great! Now let me examine the UserController file:
<function_calls>

#!/bin/bash

Read the UserController file

cat -n ./src/main/kotlin/upbrella/be/user/controller/UserController.kt | sed -n '1,150p'


</function_calls>

Perfect! Now let me check the OauthLoginService to understand how state is handled:
<function_calls>

#!/bin/bash

Find and examine OauthLoginService

find . -name "OauthLoginService" -o -name "oauthService*" | grep -v ".class"


</function_calls>

<function_calls>

#!/bin/bash

Find oauth service files

find . -type f -name "ervice.kt" | grep -i oauth | head -10


</function_calls>

Let me search for the service classes:
<function_calls>

#!/bin/bash
find . -type d -name "service" | head -5


</function_calls>

<function_calls>

#!/bin/bash

List service directory

ls -la ./src/main/kotlin/upbrella/be/*/service/ 2>/dev/null | head -100


</function_calls>

<function_calls>

#!/bin/bash

Find all Kotlin files in service directories

find ./src/main/kotlin -type f -name "*Service.kt" 2>/dev/null | head -20


</function_calls>

<function_calls>

#!/bin/bash

Search for OauthLoginService

rg "class OauthLoginService" -t kt -B 2 -A 20 2>/dev/null


</function_calls>

Let me search more broadly for oauth:
<function_calls>

#!/bin/bash
rg "processAppleLogin|getOauthToken" -t kt 2>/dev/null | head -20


</function_calls>

Let me check the entire service directory structure:
<function_calls>

#!/bin/bash
find ./src/main/kotlin -type d | head -30


</function_calls>

Let me look for the oauth package:
<function_calls>

#!/bin/bash
find ./src/main/kotlin -type d -name "oauth" -o -type d -name "auth"


</function_calls>

Let me examine the UserController imports to understand where oauthLoginService comes from:
<function_calls>

#!/bin/bash

Check the beginning of UserController to see imports

cat ./src/main/kotlin/upbrella/be/user/controller/UserController.kt | head -50


</function_calls>

<function_calls>

#!/bin/bash

Search for OauthLoginService anywhere in the codebase

rg "OauthLoginService" -t kt 2>/dev/null | head -20


</function_calls>

Let me check what's imported:
<function_calls>

#!/bin/bash

Get imports from UserController

grep -n "^import" ./src/main/kotlin/upbrella/be/user/controller/UserController.kt | grep -i oauth


</function_calls>

<function_calls>

#!/bin/bash

Get all imports

grep -n "^import" ./src/main/kotlin/upbrella/be/user/controller/UserController.kt


</function_calls>

<function_calls>

#!/bin/bash

Search for files containing OauthLoginService class definition

rg "class.*OauthLoginService" -t kt 2>/dev/null


</function_calls>

Let me search with a wildcard:
<function_calls>

#!/bin/bash

Search for interface or class definitions

rg "^class|^interface|^object" -t kt | grep -i oauth


</function_calls>

Let me look for service definitions:
<function_calls>

#!/bin/bash

Find all service classes

find ./src/main/kotlin -name "*Service.kt" -type f


</function_calls>

Let me check each service file:
<function_calls>

#!/bin/bash

Get file list

find ./src/main/kotlin -name "*Service.kt" -type f | xargs -I {} sh -c 'echo "=== {} ===" && head -20 "{}"'


</function_calls>

Let me look at all Kotlin files in the oauth package (if it exists):
<function_calls>

#!/bin/bash

List all directories

find ./src/main/kotlin/upbrella/be -type d 2>/dev/null


</function_calls>

<function_calls>

#!/bin/bash

Search in lent/login/oauth directories

ls -la ./src/main/kotlin/upbrella/be/lent/ 2>/dev/null || echo "lent dir not found"
ls -la ./src/main/kotlin/upbrella/be/login/ 2>/dev/null || echo "login dir not found"
ls -la ./src/main/kotlin/upbrella/be/oauth/ 2>/dev/null || echo "oauth dir not found"


</function_calls>

Let me search all Kotlin files:
<function_calls>

#!/bin/bash

Find all Kotlin files and search for the service class

find ./src/main/kotlin -name "*.kt" -type f -exec grep -l "OauthLoginService" {} ;


</function_calls>

Let me now check for other OAuth callbacks to understand the pattern:
<function_calls>

#!/bin/bash

Search for other login callbacks (google, kakao)

grep -n "LoginCallback|login.*callback" ./src/main/kotlin/upbrella/be/user/controller/UserController.kt -i


</function_calls>

Let me view more of the UserController file:
<function_calls>

#!/bin/bash

Get line count and view more sections

wc -l ./src/main/kotlin/upbrella/be/user/controller/UserController.kt


</function_calls>

<function_calls>

#!/bin/bash

View the full file to understand context

cat ./src/main/kotlin/upbrella/be/user/controller/UserController.kt


</function_calls>

Now let me search for the OauthLoginService:
<function_calls>

#!/bin/bash

Search all Kotlin files for OauthLoginService class or interface

rg "class|interface|object" -t kt -A 2 | grep -B 2 "OauthLoginService"


</function_calls>

Let me search differently:
<function_calls>

#!/bin/bash

Look for all files with OAuth in name

find . -name "oauth" -o -name "OAuth" 2>/dev/null | grep -v ".git" | head -20


</function_calls>

<function_calls>

#!/bin/bash

Search for login package

find ./src/main/kotlin -type d | grep -i login


</function_calls>

<function_calls>

#!/bin/bash

Check what's in the lent directory

ls -la ./src/main/kotlin/upbrella/be/lent/ 2>/dev/null


</function_calls>

<function_calls>

#!/bin/bash

Find all directories under be

find ./src/main/kotlin/upbrella/be -maxdepth 1 -type d


</function_calls>

<function_calls>

#!/bin/bash

List all subdirectories with service subdir

find ./src/main/kotlin/upbrella/be -name "service" -type d -exec ls {} ;


</function_calls>

<function_calls>

#!/bin/bash

Get lent/service directory contents

ls -la ./src/main/kotlin/upbrella/be/lent/service/


</function_calls>

<function_calls>

#!/bin/bash

List all files in that service directory

find ./src/main/kotlin/upbrella/be/lent/service -name "*.kt" -type f


</function_calls>

Great! Let me examine the OauthLoginService:
<function_calls>

#!/bin/bash

Read OauthLoginService

cat -n ./src/main/kotlin/upbrella/be/lent/service/OauthLoginService.kt


</function_calls>

Perfect! Now let me search for where the Apple login is initiated to understand state handling:
<function_calls>

#!/bin/bash

Search for Apple login initiation endpoints

grep -rn "@.*Mapping.*apple|/auth/apple" ./src/main/kotlin -i | head -20


</function_calls>

<function_calls>

#!/bin/bash

Search for any state-related session handling

grep -rn "state|State" ./src/main/kotlin/upbrella/be/user -i | head -30


</function_calls>

Let me search for state in the entire codebase:
<function_calls>

#!/bin/bash

Search for session state validation

grep -rn "session.*state|state.*session|getAttribute.*state|setAttribute.*state" ./src/main/kotlin -i | head -20


</function_calls>

<function_calls>

#!/bin/bash

Search for CSRF protection or state validation

grep -rn "csrf|CSRF|state.*validation|validate.*state" ./src/main/kotlin -i | head -20


</function_calls>

Now let me check if there's a login initiation endpoint:
<function_calls>

#!/bin/bash

Search for any mapping that initiates Apple login

grep -rn "GetMapping|PostMapping|@.*Mapping" ./src/main/kotlin/upbrella/be/user -B 2 -A 2 | grep -E "apple|Apple|login|Login" -i


</function_calls>

Let me check the configuration files:
<function_calls>

#!/bin/bash

Find and check configuration files

find . -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" | head -10


</function_calls>

<function_calls>

#!/bin/bash

Check if properties file exists and examine it

cat ./src/main/resources/application.yml 2>/dev/null || cat ./src/main/resources/application.properties 2>/dev/null || cat ./src/main/resources/application.yaml 2>/dev/null


</function_calls>

Let me check the resources directory:
<function_calls>

#!/bin/bash

List resources directory

ls -la ./src/main/resources/ 2>/dev/null | head -20


</function_calls>

<function_calls>

#!/bin/bash

Find all config files

find ./src -name ".yml" -o -name ".yaml" -o -name "*.properties" 2>/dev/null


</function_calls>

Let me check what controllers exist and look for others:
<function_calls>

#!/bin/bash

Find all controller files

find ./src -name "*Controller.kt" 2>/dev/null


</function_calls>

Let me examine the google login callback to see if it has the same pattern:
<function_calls>

#!/bin/bash

View lines around googleLoginCallback

sed -n '130,180p' ./src/main/kotlin/upbrella/be/user/controller/UserController.kt


</function_calls>

<function_calls>

#!/bin/bash

View the rest of the file

sed -n '180,210p' ./src/main/kotlin/upbrella/be/user/controller/UserController.kt


</function_calls>

Let me check the OauthInfo classes:
<function_calls>

#!/bin/bash

Search for OauthInfo class

find ./src -name "OauthInfo" -o -name "oauthinfo*" -i


</function_calls>

<function_calls>

#!/bin/bash

Search for AppleOauthInfo

grep -rn "AppleOauthInfo|class.*OauthInfo" ./src/main/kotlin -i | head -20


</function_calls>

Let me search for configuration classes:
<function_calls>

#!/bin/bash

Find config or entity files

find ./src/main/kotlin -name "*Config.kt" -o -name "*Property.kt" -o -name "*Entity.kt" | head -20


</function_calls>

<function_calls>

#!/bin/bash

List all files in lent directory

find ./src/main/kotlin/upbrella/be/lent -name "*.kt" -type f


</function_calls>

<function_calls>

#!/bin/bash

Check config package

ls -la ./src/main/kotlin/upbrella/be/lent/config/ 2>/dev/null || echo "No config dir found"


</function_calls>

<function_calls>

#!/bin/bash

Find all files in lent

find ./src/main/kotlin/upbrella/be/lent -type f -name "*.kt" | xargs ls -la


</function_calls>

<function_calls>

#!/bin/bash

Show directory structure

find ./src/main/kotlin/upbrella/be/lent -type d


</function_calls>

<function_calls>

#!/bin/bash

View the OauthProperty file

cat ./src/main/kotlin/upbrella/be/lent/config/property/OauthProperty.kt


</function_calls>

Perfect! Now let me check if there's state validation:
<function_calls>

#!/bin/bash

Search for CSRF token, state generation, or validation

grep -rn "UUID|random|state|csrf" ./src/main/kotlin/upbrella/be/lent -i | head -30


</function_calls>

Let me check for @value or @ConfigurationProperties usage:
<function_calls>

#!/bin/bash

Check how OauthProperty is used

grep -rn "OauthProperty|@value" ./src/main/kotlin/upbrella/be -i | head -20


</function_calls>

Let me check the entire lent package structure:
<function_calls>

#!/bin/bash

List all Kotlin files in lent package

find ./src/main/kotlin/upbrella/be/lent -name "*.kt" -type f -exec echo "=== {} ===" ; -exec head -20 {} ;


</function_calls>

<function_calls>

#!/bin/bash

Get list of files

find ./src/main/kotlin/upbrella/be/lent -name "*.kt" -type f


</function_calls>

Let me examine each file:
<function_calls>

#!/bin/bash

OauthProperty (already showed)

Check OauthToken

echo "=== OauthToken ==="
cat ./src/main/kotlin/upbrella/be/lent/config/property/OauthToken.kt

echo ""
echo "=== AppleOauthUser ==="
cat ./src/main/kotlin/upbrella/be/lent/config/property/AppleOauthUser.kt


</function_calls>

<function_calls>

#!/bin/bash

Check remaining files

echo "=== AppleOauthInfo ==="
cat ./src/main/kotlin/upbrella/be/lent/config/property/AppleOauthInfo.kt

echo ""
echo "=== GoogleOauthInfo ==="
cat ./src/main/kotlin/upbrella/be/lent/config/property/GoogleOauthInfo.kt 2>/dev/null || echo "Not found"


</function_calls>

<function_calls>

#!/bin/bash

Check for any other oauth info classes

find ./src/main/kotlin -name "OauthInfo" -o -name "oauthInfo*" -i


</function_calls>

<function_calls>

#!/bin/bash

Check OauthProperty constructor details

cat ./src/main/kotlin/upbrella/be/lent/config/property/OauthProperty.kt


</function_calls>

Let me check if there's frontend URL configuration:
<function_calls>

#!/bin/bash

Search for frontend URL or redirect URL configuration

grep -rn "frontend|redirectUri|redirect.url|frontendUrl" ./src/main/kotlin -i | head -20


</function_calls>

<function_calls>

#!/bin/bash

Search in the entire codebase for the hardcoded URL

grep -rn "upbrella.co.kr" ./src --include=".kt" --include=".yml" --include=".yaml" --include=".properties"


</function_calls>

<function_calls>

#!/bin/bash

Search in test files as well to understand the patterns

find ./src -name "*.kt" -type f | xargs grep -l "upbrella.co.kr" | head -10


</function_calls>

Let me search for any other OAuth implementation in the repo for comparison:
<function_calls>

#!/bin/bash

Search for Google OAuth or Kakao OAuth

grep -rn "google.*callback|kakao.*callback|google.*login|kakao.*login" ./src/main/kotlin -i


</function_calls>

Let me check the full UserController again for Google callback:
<function_calls>

#!/bin/bash

Show lines around googleLoginCallback

sed -n '130,160p' ./src/main/kotlin/upbrella/be/user/controller/UserController.kt | cat -n


</function_calls>

Now let me search for any CSRF or state validation mechanism in the entire codebase:
<function_calls>

#!/bin/bash

Search for any CSRF or state-related code

grep -rn "state" ./src/main/kotlin/upbrella/be -i -A 2 -B 2 | head -100


</function_calls>

<function_calls>

#!/bin/bash

Check if there's any validation of the state parameter anywhere

rg "state.*validate|validate.*state|state.*check|check.*state" -i -t kt


</function_calls>

Let me check the README or documentation:
<function_calls>

#!/bin/bash

Check README or other documentation

find . -name "README*" -o -name "*.md" | head -10


</function_calls>

Let me look for any environment-specific configuration:
<function_calls>

#!/bin/bash

Find all yaml/properties files

find . -name ".yml" -o -name ".yaml" -o -name "*.properties" | head -20


</function_calls>

<function_calls>

#!/bin/bash

Try to find any application configuration

find ./src/main/resources -type f | head -20


</function_calls>

Let me check if there's a pom.xml or build.gradle:
<function_calls>

#!/bin/bash

Check for build files

ls -la | grep -E "pom.xml|build.gradle|settings.gradle"


</function_calls>

Let me check the OauthLoginService more carefully to see if it validates state:
<function_calls>

#!/bin/bash

Get the full OauthLoginService file with wc first

wc -l ./src/main/kotlin/upbrella/be/lent/service/OauthLoginService.kt


</function_calls>

<function_calls>

#!/bin/bash

View the full service file

cat -n ./src/main/kotlin/upbrella/be/lent/service/OauthLoginService.kt


</function_calls>

Perfect! Now let me verify the specific issues and create a summary. Let me also check if there are any tests that show how state should be handled:
<function_calls>

#!/bin/bash

Find test files related to OAuth

find ./src/test -name "OAuth" -o -name "oauth" -o -name "Apple" | head -10


</function_calls>

<function_calls>

#!/bin/bash

Find test files for UserController

find ./src/test -name "UserController"


</function_calls>

<function_calls>

#!/bin/bash

Find all test files

find ./src/test -name "*.kt" -type f | head -10


</function_calls>

Let me now verify the exact line numbers in the code to ensure accuracy:
<function_calls>

#!/bin/bash

Get the exact appleLoginCallback method with line numbers

sed -n '92,130p' ./src/main/kotlin/upbrella/be/user/controller/UserController.kt | cat -n


</function_calls>

Ensure CSRF state parameter validation is implemented, remove unused request parameters, and externalize hard-coded redirect URLs in OAuth callbacks

The review identifies legitimate issues that I've confirmed throughout the codebase:

  1. Missing state validation: The state parameter is accepted at line 98 but never validated. Searching the entire codebase reveals no state persistence in session or validation logic anywhere. The OauthLoginService.getOauthToken() and processAppleLogin() methods do not use or verify state. This is a genuine CSRF vulnerability—implement state parameter storage during login initiation and verification in the callback before processing the authorization code.

  2. Unused request parameters: The id_token and user parameters (line 96) are not referenced in the method body; the actual id_token comes from the OAuth token response via oauthLoginService.getOauthToken() at line 108. Remove these unused parameters from the signature to avoid confusion about which values the method actually uses.

  3. Hard-coded frontend URLs: Lines 106, 110, 116, 122, and 125 all hard-code https://upbrella.co.kr for redirects. No configuration property exists in the codebase for frontend base URL. The same pattern appears in googleLoginCallback (lines 132+). Inject a configurable frontendUrl property to support local, dev, and stage environments.

🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/user/controller/UserController.kt around lines
92-130, the appleLoginCallback method accepts but never validates the state
parameter (CSRF risk), includes unused request params id_token and user, and
uses hard-coded frontend redirect URLs; fix by implementing state validation:
store a generated state in session when initiating Apple login and compare it to
the incoming state before exchanging the code, remove id_token and user from the
method signature (use the id token from oauthLoginService.getOauthToken), and
replace hard-coded https://upbrella.co.kr redirect URLs with an injected
configurable frontendBaseUrl (read from application properties) and use that for
RedirectView targets.

Comment on lines +62 to +70
return try {
Jwts.parserBuilder()
.setSigningKey(publicKey)
.build()
.parseClaimsJws(idToken)
.body
} catch (e: Exception) {
throw IllegalArgumentException("Invalid ID token: ${e.message}", e)
}
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

Missing issuer and audience validation — security risk.

The JWT verification only validates the signature but does not verify the iss (issuer) and aud (audience) claims. Per Apple's Sign in with Apple guidelines, you must verify:

  • iss equals https://appleid.apple.com
  • aud matches your app's client_id

Without these checks, an attacker could use a valid Apple token issued for a different application.

Apply this diff to add proper claim validation:

-        return try {
-            Jwts.parserBuilder()
-                .setSigningKey(publicKey)
-                .build()
-                .parseClaimsJws(idToken)
-                .body
-        } catch (e: Exception) {
-            throw IllegalArgumentException("Invalid ID token: ${e.message}", e)
-        }
+        return try {
+            Jwts.parserBuilder()
+                .setSigningKey(publicKey)
+                .requireIssuer("https://appleid.apple.com")
+                .build()
+                .parseClaimsJws(idToken)
+                .body
+        } catch (e: Exception) {
+            throw IllegalArgumentException("Invalid ID token: ${e.message}", e)
+        }

Additionally, inject the expected clientId and validate the audience claim after parsing:

val aud = claims["aud"] as? String
if (aud != expectedClientId) {
    throw IllegalArgumentException("Invalid audience: $aud")
}

@Gwonwoo-Nam Gwonwoo-Nam merged commit 126a9ca into dev Dec 6, 2025
2 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.

3 participants