Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ dependencies {
// datadog
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-datadog'

// 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'
Comment on lines +91 to +95
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.

}

// Kotlin 컴파일 옵션 설정
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/upbrella/be/config/AuthConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class AuthConfig(
"/users/login/**",
"/users/oauth/login/**",
"/users/join/**",
"/auth/apple/**",
"/stores/**",
"/index.html",
"/error/**",
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/upbrella/be/config/ProductionCorsConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ProductionCorsConfig : WebMvcConfigurer {
"https://upbrella.co.kr",
"https://www.upbrella.co.kr",
"https://api.upbrella.co.kr",
"https://appleid.apple.com",
"http://localhost:3000",
"http://www.localhost:3000"
)
Expand Down
23 changes: 23 additions & 0 deletions src/main/kotlin/upbrella/be/config/SessionCookieConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package upbrella.be.config

import org.springframework.boot.web.servlet.ServletContextInitializer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import javax.servlet.SessionCookieConfig

@Configuration
@Profile("prod")
class SessionCookieConfig {

@Bean
fun servletContextInitializer(): ServletContextInitializer {
return ServletContextInitializer { servletContext ->
val sessionCookieConfig: SessionCookieConfig = servletContext.sessionCookieConfig
sessionCookieConfig.setSecure(true)
sessionCookieConfig.setHttpOnly(true)
sessionCookieConfig.setDomain("upbrella.co.kr")
sessionCookieConfig.setPath("/")
}
}
}
91 changes: 80 additions & 11 deletions src/main/kotlin/upbrella/be/user/controller/UserController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.servlet.view.RedirectView
import upbrella.be.rent.service.RentService
import upbrella.be.user.dto.request.JoinRequest
import upbrella.be.user.dto.request.LoginCodeRequest
import upbrella.be.user.dto.request.UpdateBankAccountRequest
import upbrella.be.user.dto.response.*
import upbrella.be.user.dto.token.AppleOauthInfo
import upbrella.be.user.dto.token.KakaoOauthInfo
import upbrella.be.user.dto.token.OauthToken
import upbrella.be.user.exception.InvalidLoginCodeException
Expand All @@ -26,6 +28,7 @@ class UserController(
private val oauthLoginService: OauthLoginService,
private val userService: UserService,
private val kakaoOauthInfo: KakaoOauthInfo,
private val appleOauthInfo: AppleOauthInfo,
private val rentService: RentService,
private val blackListService: BlackListService,
) {
Expand Down Expand Up @@ -86,16 +89,69 @@ class UserController(
))
}

@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")
}
}
Comment on lines +92 to +130
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.


@PostMapping("/users/login")
fun upbrellaLogin(session: HttpSession): ResponseEntity<CustomResponse<Unit>> {
if (session.getAttribute("kakaoUser") == null) {
throw NotSocialLoginedException("[ERROR] 카카오 로그인을 먼저 해주세요.")
val kakaoUser = session.getAttribute("kakaoUser") as? KakaoLoginResponse
val appleUser = session.getAttribute("appleUser") as? AppleLoginResponse

if (kakaoUser == null && appleUser == null) {
throw NotSocialLoginedException("[ERROR] 소셜 로그인을 먼저 해주세요.")
}

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

session.removeAttribute("kakaoUser")
session.setAttribute("user", loggedInUser)

log.info("UUL 로그인 성공")
Expand Down Expand Up @@ -125,17 +181,30 @@ class UserController(

@PostMapping("/users/join")
fun kakaoJoin(session: HttpSession, @RequestBody @Valid joinRequest: JoinRequest): ResponseEntity<CustomResponse<Unit>> {
val kakaoUser = session.getAttribute("kakaoUser") as KakaoLoginResponse?
val kakaoUser = session.getAttribute("kakaoUser") as? KakaoLoginResponse
val appleUser = session.getAttribute("appleUser") as? AppleLoginResponse

if (session.getAttribute("user") != null) {
throw LoginedMemberException("[ERROR] 이미 로그인된 상태입니다.")
}
if (kakaoUser == null) {
throw NotSocialLoginedException("[ERROR] 카카오 로그인을 먼저 해주세요.")
if (kakaoUser == null && appleUser == null) {
throw NotSocialLoginedException("[ERROR] 소셜 로그인을 먼저 해주세요.")
}

val loggedInUser = when {
kakaoUser != null -> {
val user = userService.join(kakaoUser, joinRequest)
session.removeAttribute("kakaoUser")
user
}
appleUser != null -> {
val user = userService.joinApple(appleUser, joinRequest)
session.removeAttribute("appleUser")
user
}
else -> throw NotSocialLoginedException("[ERROR] 소셜 로그인을 먼저 해주세요.")
}

val loggedInUser = userService.join(kakaoUser, joinRequest)
session.removeAttribute("kakaoId")
session.setAttribute("user", loggedInUser)

log.info("UNU 회원가입 성공")
Expand All @@ -144,7 +213,7 @@ class UserController(
.body(CustomResponse(
"success",
200,
"카카오 회원가입 성공",
"소셜 회원가입 성공",
null
))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package upbrella.be.user.dto.response

import com.fasterxml.jackson.annotation.JsonProperty

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

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

@JsonProperty("email_verified")
val emailVerified: Boolean?
)
Comment on lines +5 to +14
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.

92 changes: 92 additions & 0 deletions src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package upbrella.be.user.dto.token

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
import upbrella.be.util.AppleJwtGenerator
import javax.annotation.PostConstruct

interface AppleOauthInfo {
val clientId: String
val redirectUri: String
val loginUri: String
val clientSecret: String
}

@Component
@Profile("dev", "test")
class DevAppleOauthInfo(
@Value("\${APPLE_CLIENT_ID_DEV}")
override val clientId: String,

@Value("\${APPLE_TEAM_ID_DEV}")
private val teamId: String,

@Value("\${APPLE_KEY_ID_DEV}")
private val keyId: String,

@Value("\${APPLE_P8_KEY_PATH_DEV}")
private val p8KeyPath: String,

@Value("\${APPLE_REDIRECT_URI_DEV}")
override val redirectUri: String,

@Value("\${APPLE_LOGIN_URI_DEV}")
override val loginUri: String,

private val appleJwtGenerator: AppleJwtGenerator
) : AppleOauthInfo {
private var _clientSecret: String = ""

override val clientSecret: String
get() = _clientSecret

@PostConstruct
fun init() {
_clientSecret = appleJwtGenerator.generateClientSecret(
teamId = teamId,
keyId = keyId,
clientId = clientId,
p8KeyPath = p8KeyPath
)
}
}

@Component
@Profile("prod")
class ProdAppleOauthInfo(
@Value("\${APPLE_CLIENT_ID_PROD}")
override val clientId: String,

@Value("\${APPLE_TEAM_ID_PROD}")
private val teamId: String,

@Value("\${APPLE_KEY_ID_PROD}")
private val keyId: String,

@Value("\${APPLE_P8_KEY_PATH_PROD}")
private val p8KeyPath: String,

@Value("\${APPLE_REDIRECT_URI_PROD}")
override val redirectUri: String,

@Value("\${APPLE_LOGIN_URI_PROD}")
override val loginUri: String,

private val appleJwtGenerator: AppleJwtGenerator
) : AppleOauthInfo {
private var _clientSecret: String = ""

override val clientSecret: String
get() = _clientSecret

@PostConstruct
fun init() {
_clientSecret = appleJwtGenerator.generateClientSecret(
teamId = teamId,
keyId = keyId,
clientId = clientId,
p8KeyPath = p8KeyPath
)
}
}
3 changes: 2 additions & 1 deletion src/main/kotlin/upbrella/be/user/dto/token/OauthToken.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ data class OauthToken(
val accessToken: String = "",
val refreshToken: String = "",
val tokenType: String = "",
val expiresIn: Long = 0
val expiresIn: Long = 0,
val idToken: String? = null // Apple OAuth에서 사용
)
19 changes: 19 additions & 0 deletions src/main/kotlin/upbrella/be/user/entity/User.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package upbrella.be.user.entity

import upbrella.be.user.dto.request.JoinRequest
import upbrella.be.user.dto.response.AppleLoginResponse
import upbrella.be.user.dto.response.KakaoLoginResponse
import upbrella.be.util.AesEncryptor
import upbrella.be.util.BaseTimeEntity
Expand All @@ -15,6 +16,7 @@ class User(
var name: String,
var phoneNumber: String,
var email: String,
var provider: String = "KAKAO", // KAKAO or APPLE
var adminStatus: Boolean = false,
var bank: String? = null,
var accountNumber: String? = null,
Expand All @@ -34,6 +36,23 @@ class User(
name = joinRequest.name,
phoneNumber = joinRequest.phoneNumber,
email = kakaoUser.kakaoAccount?.email ?: "",
provider = "KAKAO",
bank = aesEncryptor.encrypt(joinRequest.bank),
accountNumber = aesEncryptor.encrypt(joinRequest.accountNumber)
)
}

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)
)
Expand Down
Loading