-
Notifications
You must be signed in to change notification settings - Fork 4
feat: apple login #503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: apple login #503
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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, | ||
| ) { | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exception is swallowed - original exception context is lost. Per static analysis: the try {
appleAccessToken = oauthLoginService.getOauthToken(code.code, appleOauthInfo)!!
} catch (e: HttpClientErrorException) {
- throw InvalidLoginCodeException("[ERROR] 로그인 코드가 유효하지 않습니다.")
+ throw InvalidLoginCodeException("[ERROR] 로그인 코드가 유효하지 않습니다.", e)
}This requires
🧰 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 |
||
|
|
||
| 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 로그인 성공") | ||
|
|
@@ -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 회원가입 성공") | ||
|
|
@@ -144,7 +195,7 @@ class UserController( | |
| .body(CustomResponse( | ||
| "success", | ||
| 200, | ||
| "카카오 회원가입 성공", | ||
| "소셜 회원가입 성공", | ||
| null | ||
| )) | ||
| } | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider making Apple's Either make 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 |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Maximum validity is 6 months (must not exceed 15,777,000 seconds from iat). [1][2] Sources: 🌐 Web query:
💡 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:
🏁 Script executed: # First, let's examine the full AppleOauthInfo.kt file to understand the implementation
fd AppleOauthInfo.ktRepository: UPbrella/UPbrella_back Length of output: 126 🏁 Script executed: cat -n src/main/kotlin/upbrella/be/user/dto/token/AppleOauthInfo.ktRepository: 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 -20Repository: UPbrella/UPbrella_back Length of output: 135 🏁 Script executed: cat -n build.gradle | head -100Repository: UPbrella/UPbrella_back Length of output: 4018 🏁 Script executed: # Find and examine AppleJwtGenerator
fd AppleJwtGenerator.ktRepository: 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 -20Repository: UPbrella/UPbrella_back Length of output: 92 🏁 Script executed: cat -n src/main/kotlin/upbrella/be/util/AppleJwtGenerator.ktRepository: UPbrella/UPbrella_back Length of output: 2444 🏁 Script executed: # Search for scheduled tasks with correct syntax
rg -i "scheduled|@scheduled" --type=kotlinRepository: 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: Implement one of:
Additionally, upgrade from the deprecated 🤖 Prompt for AI Agents |
||
| } | ||
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
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
References
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:
Notes:
References
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:
BouncyCastle 1.70:
Update the snippet to:
🤖 Prompt for AI Agents