Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
73 changes: 62 additions & 11 deletions src/main/kotlin/upbrella/be/user/controller/UserController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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 +27,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 +88,52 @@ class UserController(
))
}

@PostMapping("/users/oauth/apple/login")
fun appleLogin(session: HttpSession, @RequestBody code: LoginCodeRequest): ResponseEntity<CustomResponse<Unit>> {
val appleAccessToken: OauthToken

try {
appleAccessToken = oauthLoginService.getOauthToken(code.code, appleOauthInfo)!!
} catch (e: HttpClientErrorException) {
throw InvalidLoginCodeException("[ERROR] 로그인 코드가 유효하지 않습니다.")
}
Comment on lines 104 to 112
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.


val appleLoggedInUser = oauthLoginService.processAppleLogin(appleAccessToken.accessToken, appleOauthInfo.loginUri)
session.setAttribute("appleUser", appleLoggedInUser)

return ResponseEntity
.ok()
.body(CustomResponse(
"success",
200,
"애플 로그인 성공",
null
))
}

@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 +163,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 +195,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.

45 changes: 45 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,45 @@
package upbrella.be.user.dto.token

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

@Component
open class AppleOauthInfo(
@Value("\${APPLE_CLIENT_ID_DEV}")
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}")
val redirectUri: String,

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

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

val clientSecret: String
get() = _clientSecret

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

}
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
20 changes: 17 additions & 3 deletions src/main/kotlin/upbrella/be/user/service/OauthLoginService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import org.springframework.stereotype.Service
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import org.springframework.web.client.RestTemplate
import upbrella.be.user.dto.response.AppleLoginResponse
import upbrella.be.user.dto.response.KakaoLoginResponse
import upbrella.be.user.dto.token.AppleOauthInfo
import upbrella.be.user.dto.token.KakaoOauthInfo
import upbrella.be.user.dto.token.OauthToken

Expand All @@ -15,6 +17,14 @@ class OauthLoginService(
) {

fun getOauthToken(code: String, oauthInfo: KakaoOauthInfo): OauthToken? {
return getOauthTokenInternal(code, oauthInfo.clientId, oauthInfo.clientSecret, oauthInfo.redirectUri)
}

fun getOauthToken(code: String, oauthInfo: AppleOauthInfo): OauthToken? {
return getOauthTokenInternal(code, oauthInfo.clientId, oauthInfo.clientSecret, oauthInfo.redirectUri)
}

private fun getOauthTokenInternal(code: String, clientId: String, clientSecret: String, redirectUri: String): OauthToken? {
val headers: MultiValueMap<String, String> = LinkedMultiValueMap<String, String>().apply {
setAll(
mapOf(
Expand All @@ -28,15 +38,15 @@ class OauthLoginService(
setAll(
mapOf(
"grant_type" to "authorization_code",
"client_id" to oauthInfo.clientId,
"client_secret" to oauthInfo.clientSecret,
"client_id" to clientId,
"client_secret" to clientSecret,
"code" to code
)
)
}

val request = HttpEntity(requestPayloads, headers)
val response = restTemplate.postForEntity(oauthInfo.redirectUri, request, OauthToken::class.java)
val response = restTemplate.postForEntity(redirectUri, request, OauthToken::class.java)

return response.body
}
Expand All @@ -62,4 +72,8 @@ class OauthLoginService(
fun processKakaoLogin(accessToken: String, loginUri: String): KakaoLoginResponse? {
return processLogin(accessToken, loginUri, KakaoLoginResponse::class.java)
}

fun processAppleLogin(accessToken: String, loginUri: String): AppleLoginResponse? {
return processLogin(accessToken, loginUri, AppleLoginResponse::class.java)
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/upbrella/be/user/service/UserService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import upbrella.be.rent.service.RentService
import upbrella.be.user.dto.request.JoinRequest
import upbrella.be.user.dto.request.UpdateBankAccountRequest
import upbrella.be.user.dto.response.AllUsersInfoResponse
import upbrella.be.user.dto.response.AppleLoginResponse
import upbrella.be.user.dto.response.KakaoLoginResponse
import upbrella.be.user.dto.response.SessionUser
import upbrella.be.user.dto.response.UmbrellaBorrowedByUserResponse
Expand Down Expand Up @@ -36,6 +37,12 @@ class UserService(
return SessionUser.fromUser(foundUser)
}

fun loginApple(appleSub: String): SessionUser {
val foundUser = userReader.findBySocialId(appleSub.hashCode().toLong())

return SessionUser.fromUser(foundUser)
}

fun join(kakaoUser: KakaoLoginResponse, joinRequest: JoinRequest): SessionUser {
val socialIdHash = kakaoUser.id.hashCode().toLong()

Expand All @@ -54,6 +61,24 @@ class UserService(
return SessionUser.fromUser(joinedUser)
}

fun joinApple(appleUser: AppleLoginResponse, joinRequest: JoinRequest): SessionUser {
val socialIdHash = appleUser.sub.hashCode().toLong()

if (userReader.existsBySocialId(socialIdHash)) {
throw ExistingMemberException("[ERROR] 이미 가입된 회원입니다. 로그인 폼으로 이동합니다.")
}

if (blackListReader.existsBySocialId(socialIdHash)) {
throw BlackListUserException("[ERROR] 정지된 회원입니다. 정지된 회원은 재가입이 불가능합니다.")
}

val joinedUser = userWriter.save(
User.createNewAppleUser(appleUser, joinRequest, aesEncryptor)
)

return SessionUser.fromUser(joinedUser)
}

fun findUsers(): AllUsersInfoResponse {
val users = userReader.findAll()
users.forEach { it.decryptData(aesEncryptor) }
Expand Down
62 changes: 62 additions & 0 deletions src/main/kotlin/upbrella/be/util/AppleJwtGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package upbrella.be.util

import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
import org.springframework.core.io.ClassPathResource
import org.springframework.stereotype.Component
import java.io.StringReader
import java.security.PrivateKey
import java.util.*

@Component
class AppleJwtGenerator {

/**
* Apple p8 키 파일로 JWT Client Secret 생성
*
* @param teamId Apple Developer Team ID (10자리)
* @param keyId p8 키 파일의 Key ID
* @param clientId Apple Service ID (com.example.app)
* @param p8KeyPath 클래스패스 내 p8 파일 경로 (예: "keys/AuthKey_ABCD123456.p8")
* @return JWT 토큰 문자열
*/
fun generateClientSecret(
teamId: String,
keyId: String,
clientId: String,
p8KeyPath: String
): String {
val now = Date()
val expirationTime = Date(now.time + 15777000000L) // 6개월 (최대)

val privateKey = loadPrivateKey(p8KeyPath)

return Jwts.builder()
.setHeaderParam("kid", keyId)
.setHeaderParam("alg", "ES256")
.setIssuer(teamId)
.setIssuedAt(now)
.setExpiration(expirationTime)
.setAudience("https://appleid.apple.com")
.setSubject(clientId)
.signWith(privateKey, SignatureAlgorithm.ES256)
.compact()
}

/**
* p8 파일에서 PrivateKey 로드
*/
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)
}
}
Loading
Loading