[feat] 애플 소셜로그인(IOS) 추가 - #367
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- markAsDeleted() 호출 전 apple_ prefix 체크로 순서 수정 - authorizationCode 교환 실패 로그에서 Apple 토큰 응답 전체 노출 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AppleLoginApiTest: POST /auth/apple 신규/기존 유저 분기, 유효성 검증 - UserSignupServiceAppleTest: 회원가입 시 Redis → DB refresh_token 이동 - UserDeleteServiceAppleTest: 탈퇴 시 Apple revoke 호출 여부 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WalkthroughApple 소셜 로그인 API와 identity token 검증, authorization code 교환, refresh token 저장·철회 기능을 추가했습니다. 신규 가입·기존 로그인·입력 검증·가입 및 탈퇴 연동 테스트도 포함되었습니다. ChangesApple 소셜 로그인
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant AuthController
participant AppleIdentityTokenVerifier
participant AppleTokenClient
participant AppleRefreshTokenStore
participant UserJpaRepository
Client->>AuthController: POST /auth/apple
AuthController->>AppleIdentityTokenVerifier: verify(identityToken)
AppleIdentityTokenVerifier-->>AuthController: oauth2Id
AuthController->>AppleTokenClient: exchangeAuthorizationCode(authorizationCode)
AppleTokenClient-->>AuthController: refresh_token
AuthController->>UserJpaRepository: find existing user
AuthController->>AppleRefreshTokenStore: save for new user
AuthController-->>Client: AccessToken or SignupToken
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Test Results498 tests 498 ✅ 46s ⏱️ Results for commit 6c0affd. |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (1)
src/test/java/konkuk/thip/common/security/oauth2/auth/AppleLoginApiTest.java (1)
60-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winrefresh token 저장 결과도 검증하세요.
현재 테스트는 응답만 확인하므로 Redis 저장과 기존 사용자 DB 갱신이 제거돼도 통과합니다. 신규 사용자에서는
AppleRefreshTokenStore.save(oauth2Id, refreshToken)을, 기존 사용자에서는 요청 후 조회한 엔티티의appleRefreshToken값을 검증하세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/konkuk/thip/common/security/oauth2/auth/AppleLoginApiTest.java` around lines 60 - 93, Update appleLogin_newUser_returnsSignupToken and appleLogin_existingUser_returnsAccessToken to verify refresh-token persistence in addition to the response. For the new-user case, assert AppleRefreshTokenStore.save is called with the OAuth2 ID and refresh token; for the existing-user case, reload the user entity after the request and assert its appleRefreshToken value was updated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/konkuk/thip/common/security/oauth2/apple/AppleIdentityTokenVerifier.java`:
- Around line 40-41: AppleIdentityTokenVerifier의 sub 로깅을 제거하세요.
claims.getSubject()로 값을 얻는 로직은 유지하되, 로그인 성공 여부나 요청 상관관계 ID만 기록하도록 log.info 호출을
수정하세요.
- Around line 27-42: Update AppleIdentityTokenVerifier.verify to enforce the
issuer https://appleid.apple.com and the configured clientId audience after
signature verification, and reject tokens whose subject is null or empty before
logging or returning it. Reuse the existing configuration symbol for clientId
and preserve the current verification failure handling.
In
`@src/main/java/konkuk/thip/common/security/oauth2/apple/AppleRefreshTokenStore.java`:
- Around line 22-28: Update AppleRefreshTokenStore.pop to replace the separate
redisTemplate.opsForValue().get and conditional delete calls with a single
ValueOperations#getAndDelete operation, preserving the existing key construction
and return value while ensuring each temporary refresh token is consumed only
once.
In
`@src/main/java/konkuk/thip/common/security/oauth2/apple/AppleTokenClient.java`:
- Around line 39-53: Update the Apple token exchange method around the
restTemplate.postForObject call to propagate missing refresh_token responses and
caught exceptions to the caller instead of logging and returning null. Update
the related revocation flow around the additional affected block so account
deletion proceeds only after revocation succeeds, or persist a durable retry
task when revocation fails.
In `@src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java`:
- Around line 95-106: Update the authorizationCode flow in AuthController so a
null result from exchangeAuthorizationCode is not treated as successful
authentication or followed by SignupToken issuance. Return an authentication
error for exchange failure, or persist a retryable state that preserves the
pending authorization safely; ensure this applies to both existing and new
users.
In `@src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java`:
- Around line 42-43:
src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java:42-43에서
appleRefreshToken을 평문 String으로 저장하지 않도록 암·복호화를 담당하는 값 객체 또는 JPA converter를 적용하고,
복호화는 revoke 처리 시점에만 수행하세요.
src/main/resources/db/migration/V260722__Add_apple_refresh_token.sql:1-2에서는 해당
암호문 형식과 암호화된 값의 길이를 수용하도록 컬럼 타입 또는 길이를 변경하세요.
In `@src/main/java/konkuk/thip/user/application/service/UserDeleteService.java`:
- Around line 61-65: Update the Apple deletion flow in UserDeleteService around
appleTokenClient.revokeToken so account deletion and Apple token revocation are
not executed as one best-effort transaction. Persist the deletion state and a
retryable outbox/task during the database transaction, then move revocation to a
post-commit worker that records failures and retries them; ensure deletion
remains consistent even when revocation or subsequent cleanup fails.
In `@src/main/java/konkuk/thip/user/application/service/UserSignupService.java`:
- Around line 48-58: Update the Apple refresh-token handling in
UserSignupService so it does not call the destructive appleRefreshTokenStore.pop
before the database transaction commits. Read the token non-destructively,
persist it through userJpaRepository, and remove it from Redis only after a
successful commit, or use an equivalent retry-safe outbox/state transition.
---
Nitpick comments:
In
`@src/test/java/konkuk/thip/common/security/oauth2/auth/AppleLoginApiTest.java`:
- Around line 60-93: Update appleLogin_newUser_returnsSignupToken and
appleLogin_existingUser_returnsAccessToken to verify refresh-token persistence
in addition to the response. For the new-user case, assert
AppleRefreshTokenStore.save is called with the OAuth2 ID and refresh token; for
the existing-user case, reload the user entity after the request and assert its
appleRefreshToken value was updated.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a94895ad-1fa4-4040-978f-23d72a0c8569
📒 Files selected for processing (17)
src/main/java/konkuk/thip/common/exception/code/ErrorCode.javasrc/main/java/konkuk/thip/common/security/constant/AuthParameters.javasrc/main/java/konkuk/thip/common/security/constant/SecurityWhitelist.javasrc/main/java/konkuk/thip/common/security/oauth2/apple/AppleClientSecretGenerator.javasrc/main/java/konkuk/thip/common/security/oauth2/apple/AppleIdentityTokenVerifier.javasrc/main/java/konkuk/thip/common/security/oauth2/apple/AppleLoginRequest.javasrc/main/java/konkuk/thip/common/security/oauth2/apple/AppleProperties.javasrc/main/java/konkuk/thip/common/security/oauth2/apple/AppleRefreshTokenStore.javasrc/main/java/konkuk/thip/common/security/oauth2/apple/AppleTokenClient.javasrc/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.javasrc/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.javasrc/main/java/konkuk/thip/user/application/service/UserDeleteService.javasrc/main/java/konkuk/thip/user/application/service/UserSignupService.javasrc/main/resources/db/migration/V260722__Add_apple_refresh_token.sqlsrc/test/java/konkuk/thip/common/security/oauth2/auth/AppleLoginApiTest.javasrc/test/java/konkuk/thip/user/application/service/UserDeleteServiceAppleTest.javasrc/test/java/konkuk/thip/user/application/service/UserSignupServiceAppleTest.java
| public AppleIdentityTokenVerifier() { | ||
| try { | ||
| JWKSource<SecurityContext> keySource = new RemoteJWKSet<>(new URL(APPLE_JWK_SET_URI)); | ||
| jwtProcessor = new DefaultJWTProcessor<>(); | ||
| jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, keySource)); | ||
| } catch (Exception e) { | ||
| throw new IllegalStateException("Apple JWK 초기화 실패", e); | ||
| } | ||
| } | ||
|
|
||
| public String verify(String identityToken) { | ||
| try { | ||
| JWTClaimsSet claims = jwtProcessor.process(identityToken, null); | ||
| String sub = claims.getSubject(); | ||
| log.info("[Apple Login] verified sub={}", sub); | ||
| return sub; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
리포지터리에서 사용하는 Nimbus JOSE+JWT 버전의 DefaultJWTProcessor로 Apple ID token의 issuer와 aud(client ID)를 검증하는 권장 설정은 무엇인가요?
💡 Result:
Apple ID 토큰을 검증하기 위해 Nimbus JOSE+JWT의 DefaultJWTProcessor를 사용할 때, issuer("iss")와 audience("aud")를 검증하는 권장 방법은 DefaultJWTClaimsVerifier를 설정하여 DefaultJWTProcessor에 주입하는 것입니다 [1][2][3]. 애플의 공식 요구사항에 따라, iss 클레임은 반드시 https://appleid.apple.com 이어야 하며, aud 클레임은 개발자의 client_id와 일치해야 합니다 [4][5]. 권장 구현 설정 예시는 다음과 같습니다: 1. JWTClaimsSet을 사용하여 필수적인 iss와 aud 값을 설정합니다 [1][3]. 2. DefaultJWTClaimsVerifier를 생성하고, 이 verifier를 JWTProcessor에 설정합니다 [1][6][7]. // 1. 검증할 정확한 클레임 값 설정 (iss, aud) JWTClaimsSet expectedClaims = new JWTClaimsSet.Builder.issuer("https://appleid.apple.com").audience("YOUR_CLIENT_ID") // 애플의 client_id.build; // 2. 검증기 생성 (필요에 따라 "exp"와 같은 필수 클레임도 함께 명시) DefaultJWTClaimsVerifier<?> claimsVerifier = new DefaultJWTClaimsVerifier<>( expectedClaims, new HashSet<>(Arrays.asList("exp", "sub", "iat")) // 필수 포함 클레임 명시); // 3. 프로세서에 설정 ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>; jwtProcessor.setJWTClaimsSetVerifier(claimsVerifier); 또한, 토큰의 서명을 검증하기 위해서는 Apple의 공개 키(JWKSet)를 사용하여 JWSKeySelector를 설정해야 합니다 [8][9]. 추가적으로 토큰의 typ 헤더를 명시적으로 검증하는 것이 보안상 권장됩니다 [2][9].
Citations:
- 1: https://www.javadoc.io/static/com.nimbusds/nimbus-jose-jwt/10.4.1/com/nimbusds/jwt/proc/DefaultJWTClaimsVerifier.html
- 2: https://connect2id.com/blog/nimbus-jose-jwt-8
- 3: https://javadoc.io/static/com.nimbusds/nimbus-jose-jwt/10.0/com/nimbusds/jwt/proc/DefaultJWTClaimsVerifier.html
- 4: https://developer.apple.com/documentation/signinwithapple/verifying-a-user
- 5: https://developer.apple.com/documentation/sign_in_with_apple/authenticating-users-with-sign-in-with-apple
- 6: https://stackoverflow.com/questions/71263642/nimbus-jose-jwt-expected-audience-claim-to-be-any-of-a-multiple
- 7: https://stackoverflow.com/questions/63069619/validating-jwt-claims-using-nimbus-jose-jwt-java
- 8: https://developer.apple.com/documentation/signinwithapplerestapi/fetch-apple's-public-key-for-verifying-token-signature
- 9: https://javadoc.io/static/com.nimbusds/nimbus-jose-jwt/9.9.2/com/nimbusds/jwt/proc/DefaultJWTProcessor.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== AppleIdentityTokenVerifier ==\n'
sed -n '1,220p' src/main/java/konkuk/thip/common/security/oauth2/apple/AppleIdentityTokenVerifier.java
printf '\n== Search verifier usage ==\n'
rg -n "AppleIdentityTokenVerifier|appleLogin|identityToken|clientId|aud|issuer|iss|sub" src/main/java -g '!**/target/**'Repository: THIP-TextHip/THIP-Server
Length of output: 8373
iss와 aud를 함께 검증하세요
지금은 Apple JWK/RS256 서명만 확인해서, 다른 clientId용으로 발급된 유효한 Apple 토큰도 통과할 수 있습니다. https://appleid.apple.com issuer와 설정된 clientId audience를 서명 검증 뒤에 강제하고, sub가 비어 있지 않은지도 확인하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/konkuk/thip/common/security/oauth2/apple/AppleIdentityTokenVerifier.java`
around lines 27 - 42, Update AppleIdentityTokenVerifier.verify to enforce the
issuer https://appleid.apple.com and the configured clientId audience after
signature verification, and reject tokens whose subject is null or empty before
logging or returning it. Reuse the existing configuration symbol for clientId
and preserve the current verification failure handling.
| String sub = claims.getSubject(); | ||
| log.info("[Apple Login] verified sub={}", sub); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Apple sub를 INFO 로그에 기록하지 마세요.
sub는 사용자를 지속적으로 식별하는 값입니다. 로그인 성공 여부만 기록하거나 요청 상관관계 ID를 사용하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/konkuk/thip/common/security/oauth2/apple/AppleIdentityTokenVerifier.java`
around lines 40 - 41, AppleIdentityTokenVerifier의 sub 로깅을 제거하세요.
claims.getSubject()로 값을 얻는 로직은 유지하되, 로그인 성공 여부나 요청 상관관계 ID만 기록하도록 log.info 호출을
수정하세요.
| public String pop(String oauth2Id) { | ||
| String key = PREFIX + oauth2Id; | ||
| String token = redisTemplate.opsForValue().get(key); | ||
| if (token != null) { | ||
| redisTemplate.delete(key); | ||
| } | ||
| return token; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
리포지터리의 Spring Data Redis 버전에서 ValueOperations#getAndDelete가 Redis GETDEL로 원자적으로 실행되는지 확인해 주세요.
💡 Result:
네, Spring Data Redis의 ValueOperations#getAndDelete 메서드는 Redis의 GETDEL 명령어를 사용하여 원자적으로(atomically) 실행됩니다 [1][2]. 구체적인 근거는 다음과 같습니다: 1. 구현 확인: Spring Data Redis의 소스 코드(DefaultValueOperations.java)를 확인하면, getAndDelete 메서드는 내부적으로 RedisConnection의 getDel 명령을 호출하도록 구현되어 있습니다 [1]. 2. Redis 명령 특성: Redis의 GETDEL 명령어는 키의 값을 가져온 후 즉시 해당 키를 삭제하는 동작을 하나의 원자적 작업으로 수행합니다 [3]. 3. 공식 문서 및 API: Spring Data Redis의 ValueOperations API 문서에서도 getAndDelete 메서드의 참고 항목(See Also)으로 Redis의 GETDEL 명령어를 명시하고 있으며, 해당 명령어를 통해 원자적 삭제를 지원함을 나타냅니다 [4][5][2]. 따라서 ValueOperations#getAndDelete를 사용하면 별도의 트랜잭션이나 Lua 스크립트 없이도 안전하게 값을 읽고 삭제하는 원자적 처리가 가능합니다.
Citations:
- 1: https://github.com/spring-projects/spring-data-redis/blob/master/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java
- 2: https://github.com/spring-projects/spring-data-redis/blob/master/src/main/java/org/springframework/data/redis/core/ValueOperations.java
- 3: https://redis.io/docs/latest/commands/getdel/
- 4: https://docs.spring.io/spring-data/data-redis/docs/current/api/org/springframework/data/redis/core/ValueOperations.html
- 5: https://docs.spring.io/spring-data/redis/docs/current/api/org/springframework/data/redis/core/ValueOperations.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Spring Data Redis version candidates =="
rg -n --hidden -S "spring-data-redis|springframework.data.redis" -g 'pom.xml' -g 'build.gradle' -g 'build.gradle.kts' -g 'gradle.properties' -g 'settings.gradle' -g 'settings.gradle.kts' .
echo
echo "== AppleRefreshTokenStore =="
sed -n '1,120p' src/main/java/konkuk/thip/common/security/oauth2/apple/AppleRefreshTokenStore.java
echo
echo "== Files mentioning getAndDelete / GETDEL =="
rg -n -S "getAndDelete|GETDEL|getDel" src/main/java .Repository: THIP-TextHip/THIP-Server
Length of output: 205
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Build files =="
git ls-files '*pom.xml' '*build.gradle' '*build.gradle.kts' '*settings.gradle' '*settings.gradle.kts' '*gradle.properties'
echo
echo "== AppleRefreshTokenStore =="
sed -n '1,160p' src/main/java/konkuk/thip/common/security/oauth2/apple/AppleRefreshTokenStore.java
echo
echo "== Search for getAndDelete / getDel / RedisTemplate delete-get patterns =="
rg -n -S "getAndDelete|getDel|opsForValue\(\)\.get\(|redisTemplate\.delete\(" src/main/javaRepository: THIP-TextHip/THIP-Server
Length of output: 1907
임시 refresh token 소비를 원자적으로 바꾸세요. pop에서 GET과 DELETE를 분리하면 동시 요청이 같은 토큰을 둘 다 가져갈 수 있습니다. ValueOperations#getAndDelete로 교체해 단일 소비를 보장하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/konkuk/thip/common/security/oauth2/apple/AppleRefreshTokenStore.java`
around lines 22 - 28, Update AppleRefreshTokenStore.pop to replace the separate
redisTemplate.opsForValue().get and conditional delete calls with a single
ValueOperations#getAndDelete operation, preserving the existing key construction
and return value while ensuring each temporary refresh token is consumed only
once.
| try { | ||
| Map<?, ?> response = restTemplate.postForObject( | ||
| APPLE_TOKEN_URL, | ||
| new HttpEntity<>(params, headers), | ||
| Map.class | ||
| ); | ||
| if (response == null || !response.containsKey("refresh_token")) { | ||
| log.warn("[Apple] authorizationCode 교환 실패: refresh_token 없음"); | ||
| return null; | ||
| } | ||
| return (String) response.get("refresh_token"); | ||
| } catch (Exception e) { | ||
| log.warn("[Apple] authorizationCode 교환 중 오류: {}", e.getMessage()); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Apple 토큰 작업 실패를 무시하지 마세요.
코드 교환 실패 시 로그인은 refresh token 없이 계속되고, 철회 실패 시 계정은 삭제되어 재시도할 근거가 사라집니다. 제공된 authorization code 교환 실패는 호출자에게 실패로 전파하고, 철회는 삭제 전에 성공을 보장하거나 내구성 있는 재시도 작업으로 저장하세요.
Also applies to: 68-73
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/konkuk/thip/common/security/oauth2/apple/AppleTokenClient.java`
around lines 39 - 53, Update the Apple token exchange method around the
restTemplate.postForObject call to propagate missing refresh_token responses and
caught exceptions to the caller instead of logging and returning null. Update
the related revocation flow around the additional affected block so account
deletion proceeds only after revocation succeeds, or persist a durable retry
task when revocation fails.
| if (request.authorizationCode() != null) { | ||
| String refreshToken = appleTokenClient.exchangeAuthorizationCode(request.authorizationCode()); | ||
| if (refreshToken != null) { | ||
| if (existingUser.isPresent()) { | ||
| // 기존 유저: DB에 바로 저장 | ||
| existingUser.get().updateAppleRefreshToken(refreshToken); | ||
| userJpaRepository.save(existingUser.get()); | ||
| } else { | ||
| // 신규 유저: 회원가입 완료 시 옮겨 저장하도록 Redis에 임시 보관 (TTL 30분) | ||
| appleRefreshTokenStore.save(oauth2Id, refreshToken); | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
authorization code 교환 실패를 로그인 성공으로 처리하지 마세요.
exchangeAuthorizationCode()는 통신 오류나 refresh_token 누락 시 null을 반환하지만, 현재는 신규 사용자에게도 SignupToken을 발급합니다. 이후 가입해도 저장·철회할 Apple refresh token이 없어집니다. authorizationCode가 전달된 경우 교환 실패를 인증 오류로 반환하거나, 재시도 가능한 영속 상태를 남기세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java`
around lines 95 - 106, Update the authorizationCode flow in AuthController so a
null result from exchangeAuthorizationCode is not treated as successful
authentication or followed by SignupToken issuance. Return an authentication
error for exchange failure, or persist a retryable state that preserves the
pending authorization safely; ensure this applies to both existing and new
users.
| @Column(name = "apple_refresh_token", length = 1000) | ||
| private String appleRefreshToken; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Apple refresh token을 평문으로 저장하지 마세요.
Apple refresh token은 장기 bearer credential인데, 현재 JPA 필드와 DB 컬럼이 원문을 그대로 보관합니다. DB 덤프나 읽기 권한 탈취 시 Apple 연결이 재사용될 수 있습니다. 애플리케이션 레벨 암호화(envelope encryption 등)를 적용하고 revoke 시점에만 복호화하세요.
src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java#L42-L43: 암·복호화되는 값 객체 또는 JPA converter로 원문 token 저장을 제거하세요.src/main/resources/db/migration/V260722__Add_apple_refresh_token.sql#L1-L2: 암호문 저장 형식과 길이에 맞는 컬럼으로 변경하세요.
📍 Affects 2 files
src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java#L42-L43(this comment)src/main/resources/db/migration/V260722__Add_apple_refresh_token.sql#L1-L2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java` around
lines 42 - 43,
src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java:42-43에서
appleRefreshToken을 평문 String으로 저장하지 않도록 암·복호화를 담당하는 값 객체 또는 JPA converter를 적용하고,
복호화는 revoke 처리 시점에만 수행하세요.
src/main/resources/db/migration/V260722__Add_apple_refresh_token.sql:1-2에서는 해당
암호문 형식과 암호화된 값의 길이를 수용하도록 컬럼 타입 또는 길이를 변경하세요.
| // Apple 유저라면 markAsDeleted() 전에 oauth2Id 확인 후 refresh_token 철회 | ||
| if (user.getOauth2Id() != null && user.getOauth2Id().startsWith("apple_")) { | ||
| userJpaRepository.findByOauth2Id(user.getOauth2Id()) | ||
| .filter(entity -> entity.getAppleRefreshToken() != null) | ||
| .ifPresent(entity -> appleTokenClient.revokeToken(entity.getAppleRefreshToken())); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Apple revoke와 계정 삭제를 best-effort로 분리하지 마세요.
revokeToken()은 오류를 삼키므로 revoke 실패 후에도 탈퇴가 커밋됩니다. 반대로 revoke 성공 뒤 후속 삭제가 실패하면 DB는 롤백되어도 Apple revoke는 되돌릴 수 없습니다. 탈퇴 상태와 revoke 작업을 영속화하고, 커밋 후 재시도 가능한 outbox/worker로 처리하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/konkuk/thip/user/application/service/UserDeleteService.java`
around lines 61 - 65, Update the Apple deletion flow in UserDeleteService around
appleTokenClient.revokeToken so account deletion and Apple token revocation are
not executed as one best-effort transaction. Persist the deletion state and a
retryable outbox/task during the database transaction, then move revocation to a
post-commit worker that records failures and retries them; ensure deletion
remains consistent even when revocation or subsequent cleanup fails.
| // Apple 유저라면 Redis에 임시 보관된 refresh_token을 DB로 옮김 | ||
| if (command.oauth2Id().startsWith("apple_")) { | ||
| String refreshToken = appleRefreshTokenStore.pop(command.oauth2Id()); | ||
| if (refreshToken != null) { | ||
| userJpaRepository.findByOauth2Id(command.oauth2Id()) | ||
| .ifPresent(entity -> { | ||
| entity.updateAppleRefreshToken(refreshToken); | ||
| userJpaRepository.save(entity); | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
DB 커밋 전에 Redis 토큰을 삭제하면 refresh token이 유실됩니다.
pop()은 즉시 Redis 키를 삭제합니다. 이후 save() 또는 트랜잭션 커밋이 실패하면 DB 생성은 롤백되지만 Redis 토큰은 복구되지 않아, 재가입 시 Apple 토큰을 영구적으로 저장·철회할 수 없습니다. 비파괴 조회 후 커밋 완료 시 삭제하거나, 재시도 가능한 outbox/상태 전이를 사용하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/konkuk/thip/user/application/service/UserSignupService.java`
around lines 48 - 58, Update the Apple refresh-token handling in
UserSignupService so it does not call the destructive appleRefreshTokenStore.pop
before the database transaction commits. Read the token non-destructively,
persist it through userJpaRepository, and remove it from Redis only after a
successful commit, or use an equivalent retry-safe outbox/state transition.
#️⃣ 연관된 이슈
📝 작업 내용
배경
iOS 앱에서 Apple 소셜 로그인을 지원하기 위한 서버 구현입니다.
기존 카카오/구글은 웹 리다이렉트(Spring Security OAuth2) 방식이지만, Apple Sign In은 iOS SDK(
AuthenticationServices)가 로그인을 직접 처리하는 네이티브 방식을 사용합니다.구현 흐름
상세 구현 내용
Apple 인증 핵심 클래스
AppleIdentityTokenVerifier— Apple JWKS 공개키로 identityToken JWT 서명 검증, sub(Apple 고유 유저ID) 추출AppleClientSecretGenerator—.p8EC 개인키 + Team ID/Key ID로 ES256 JWT client_secret 생성 (Apple 토큰 서버 인증용)AppleTokenClient— Apple 토큰 서버 HTTP 통신 (authorizationCode 교환, 토큰 철회)AppleRefreshTokenStore— 신규 유저 refresh_token Redis 임시 보관 (회원가입 완료 전 브릿지)AppleProperties— yml 바인딩 (client-id,team-id,key-id,private-key)API
POST /auth/apple— 인증 없이 접근 가능 (SecurityWhitelist 등록),identityToken필수 /authorizationCode선택DB
users.apple_refresh_token VARCHAR(1000) NULL컬럼 추가 (FlywayV260722)환경변수 (yml 주입 필요)
수정된 기존 코드
UserDeleteService— 탈퇴 시 Apple revoke 호출 추가.markAsDeleted()가 oauth2Id를deleted:apple_...으로 변경하므로 revoke를 반드시 먼저 호출UserSignupService— 회원가입 완료 시 Redis에서 Apple refresh_token 꺼내 DB 저장CustomOidcUserService,CustomOidcUser,AppleUserDetails)테스트
AppleLoginApiTest— POST /auth/apple 신규/기존 유저 분기, 유효성 검증, 토큰 검증 실패 시 401 (4개)UserSignupServiceAppleTest— Redis → DB refresh_token 이동, 토큰 없을 때, 비Apple 유저 (3개)UserDeleteServiceAppleTest— revoke 호출 확인, refresh_token 없을 때, 비Apple 유저 (3개)📸 스크린샷
해당 없음
💬 리뷰 요구사항
apple_refresh_token은 DB에 평문 저장됩니다. 공격자가 탈취해도 할 수 있는 행위가 Apple 세션 강제 종료 수준으로 제한적이나, 팀 보안 정책에 따라 AES 암호화 저장으로 전환 여부를 검토해 주세요.authorizationCode는 Apple 정책상 1회만 유효합니다. 현재 교환 실패 시 로그만 남기고 넘어가는데, 재시도 전략이 필요한지 검토 부탁드립니다.client_secretJWT는 최대 6개월 유효합니다. 현재는 요청마다 새로 생성하는 방식이며, 캐싱이 필요하면 추후 개선할 수 있는 여지가있습니다.Summary by CodeRabbit
새 기능
버그 수정
테스트