Skip to content

[feat] 애플 소셜로그인(IOS) 추가 - #367

Merged
hd0rable merged 3 commits into
developfrom
feat/#365-apple-social-login
Jul 23, 2026
Merged

[feat] 애플 소셜로그인(IOS) 추가#367
hd0rable merged 3 commits into
developfrom
feat/#365-apple-social-login

Conversation

@hd0rable

@hd0rable hd0rable commented Jul 22, 2026

Copy link
Copy Markdown
Member

#️⃣ 연관된 이슈

closes #365

📝 작업 내용

배경

iOS 앱에서 Apple 소셜 로그인을 지원하기 위한 서버 구현입니다.
기존 카카오/구글은 웹 리다이렉트(Spring Security OAuth2) 방식이지만, Apple Sign In은 iOS SDK(AuthenticationServices)가 로그인을 직접 처리하는 네이티브 방식을 사용합니다.


구현 흐름

[로그인]
iOS → Apple SDK 호출
     → identityToken (JWT) + authorizationCode 수신
iOS → POST /auth/apple { identityToken, authorizationCode }
서버 → identityToken 서명 검증 (Apple 공개키, https://appleid.apple.com/auth/keys)
     → authorizationCode → Apple 토큰 서버 교환 → refresh_token 획득
     → 신규 유저: refresh_token Redis 임시 저장(TTL 30분), SignupToken 반환
     → 기존 유저: refresh_token DB 업데이트, AccessToken 반환

[회원가입 완료]
POST /users/signup 호출 시
서버 → oauth2Id가 apple_로 시작하면 Redis에서 refresh_token pop → DB 저장

[탈퇴]
DELETE /users 호출 시
서버 → apple_refresh_token 존재하면 POST https://appleid.apple.com/auth/revoke 호출
     → Apple 정책 준수 (미이행 시 App Store 리젝 사유)

상세 구현 내용

Apple 인증 핵심 클래스

  • AppleIdentityTokenVerifier — Apple JWKS 공개키로 identityToken JWT 서명 검증, sub(Apple 고유 유저ID) 추출
  • AppleClientSecretGenerator.p8 EC 개인키 + 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 컬럼 추가 (Flyway V260722)

환경변수 (yml 주입 필요)

APPLE_CLIENT_ID    # Apple Services ID 
APPLE_TEAM_ID      # Apple Developer Team ID (10자리)
APPLE_KEY_ID       # .p8 키의 Key ID
APPLE_PRIVATE_KEY  # .p8 파일 내용 (헤더/푸터 제거, 한 줄)

수정된 기존 코드

  • UserDeleteService — 탈퇴 시 Apple revoke 호출 추가. markAsDeleted()가 oauth2Id를 deleted:apple_...으로 변경하므로 revoke를 반드시 먼저 호출
  • UserSignupService — 회원가입 완료 시 Redis에서 Apple refresh_token 꺼내 DB 저장
  • Spring Security OAuth2 Apple 웹 리다이렉트 코드 제거 (CustomOidcUserService, CustomOidcUser, AppleUserDetails)

테스트

  • AppleLoginApiTest — POST /auth/apple 신규/기존 유저 분기, 유효성 검증, 토큰 검증 실패 시 401 (4개)
  • UserSignupServiceAppleTest — Redis → DB refresh_token 이동, 토큰 없을 때, 비Apple 유저 (3개)
  • UserDeleteServiceAppleTest — revoke 호출 확인, refresh_token 없을 때, 비Apple 유저 (3개)

📸 스크린샷

해당 없음

💬 리뷰 요구사항

  • P1 apple_refresh_token은 DB에 평문 저장됩니다. 공격자가 탈취해도 할 수 있는 행위가 Apple 세션 강제 종료 수준으로 제한적이나, 팀 보안 정책에 따라 AES 암호화 저장으로 전환 여부를 검토해 주세요.
  • P2 authorizationCode는 Apple 정책상 1회만 유효합니다. 현재 교환 실패 시 로그만 남기고 넘어가는데, 재시도 전략이 필요한지 검토 부탁드립니다.
  • P2 Apple client_secret JWT는 최대 6개월 유효합니다. 현재는 요청마다 새로 생성하는 방식이며, 캐싱이 필요하면 추후 개선할 수 있는 여지가있습니다.

Summary by CodeRabbit

  • 새 기능

    • Apple 네이티브 소셜 로그인을 지원합니다.
    • 신규 가입 및 기존 회원 로그인 시 인증 토큰을 발급합니다.
    • Apple 계정의 리프레시 토큰을 안전하게 저장하고 관리합니다.
    • 회원 탈퇴 시 Apple 리프레시 토큰을 철회합니다.
  • 버그 수정

    • 유효하지 않은 Apple identity token에 대해 명확한 인증 오류를 반환합니다.
    • 필수 identity token이 누락된 요청을 검증합니다.
  • 테스트

    • Apple 로그인, 가입, 탈퇴 및 토큰 처리 시나리오를 검증하는 테스트를 추가했습니다.

hd0rable and others added 3 commits July 22, 2026 22:37
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>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Apple 소셜 로그인 API와 identity token 검증, authorization code 교환, refresh token 저장·철회 기능을 추가했습니다. 신규 가입·기존 로그인·입력 검증·가입 및 탈퇴 연동 테스트도 포함되었습니다.

Changes

Apple 소셜 로그인

Layer / File(s) Summary
Apple 인증 계약과 검증
src/main/java/konkuk/thip/common/security/..., src/main/java/konkuk/thip/common/exception/...
Apple 요청 모델과 설정을 추가하고, ES256 client secret 생성 및 Apple JWK 기반 identity token 검증을 구현했습니다.
Apple 토큰 교환과 임시 저장
src/main/java/konkuk/thip/common/security/oauth2/apple/*
authorization code 교환, refresh token 철회, Redis 기반 30분 임시 저장과 단발성 조회·삭제를 추가했습니다.
로그인 엔드포인트와 토큰 영속화
src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java, src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java, src/main/resources/db/migration/*
POST /auth/apple에서 신규·기존 사용자별 토큰을 발급하고, Apple refresh token을 DB에 저장하도록 연결했습니다.
가입·탈퇴 라이프사이클 연동 및 검증
src/main/java/konkuk/thip/user/application/service/*, src/test/java/...
가입 시 Redis 토큰을 DB로 이동하고 탈퇴 전 Apple 토큰을 철회하며, 로그인·가입·탈퇴 시나리오를 테스트했습니다.

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
Loading

Poem

당근처럼 반짝, Apple 문이 열리고
토큰은 안전한 둥지로 살포시
새 가입자는 Redis를 지나
탈퇴할 땐 토큰도 훌쩍 떠나네
깡충! 로그인 흐름이 완성됐어요 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed #365의 애플 소셜로그인 추가 요구사항을 서버 로그인, 토큰 처리, 저장, 테스트까지 반영했습니다.
Out of Scope Changes check ✅ Passed 요구사항과 무관한 변경은 보이지 않으며, 추가된 수정들은 Apple 로그인 구현에 직접 연관됩니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 애플 iOS 소셜 로그인 추가라는 핵심 변경을 잘 요약하고 있습니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#365-apple-social-login

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hd0rable hd0rable changed the title Feat/#365 apple social login [feat] 애플 소셜로그인(IOS) 추가 Jul 22, 2026
@github-actions

Copy link
Copy Markdown

Test Results

498 tests   498 ✅  46s ⏱️
148 suites    0 💤
148 files      0 ❌

Results for commit 6c0affd.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

refresh 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

📥 Commits

Reviewing files that changed from the base of the PR and between 02b04c2 and 6c0affd.

📒 Files selected for processing (17)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
  • src/main/java/konkuk/thip/common/security/constant/AuthParameters.java
  • src/main/java/konkuk/thip/common/security/constant/SecurityWhitelist.java
  • src/main/java/konkuk/thip/common/security/oauth2/apple/AppleClientSecretGenerator.java
  • src/main/java/konkuk/thip/common/security/oauth2/apple/AppleIdentityTokenVerifier.java
  • src/main/java/konkuk/thip/common/security/oauth2/apple/AppleLoginRequest.java
  • src/main/java/konkuk/thip/common/security/oauth2/apple/AppleProperties.java
  • src/main/java/konkuk/thip/common/security/oauth2/apple/AppleRefreshTokenStore.java
  • src/main/java/konkuk/thip/common/security/oauth2/apple/AppleTokenClient.java
  • src/main/java/konkuk/thip/common/security/oauth2/auth/AuthController.java
  • src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java
  • src/main/java/konkuk/thip/user/application/service/UserDeleteService.java
  • src/main/java/konkuk/thip/user/application/service/UserSignupService.java
  • src/main/resources/db/migration/V260722__Add_apple_refresh_token.sql
  • src/test/java/konkuk/thip/common/security/oauth2/auth/AppleLoginApiTest.java
  • src/test/java/konkuk/thip/user/application/service/UserDeleteServiceAppleTest.java
  • src/test/java/konkuk/thip/user/application/service/UserSignupServiceAppleTest.java

Comment on lines +27 to +42
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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


🏁 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


issaud를 함께 검증하세요
지금은 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.

Comment on lines +40 to +41
String sub = claims.getSubject();
log.info("[Apple Login] verified sub={}", sub);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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 호출을
수정하세요.

Comment on lines +22 to +28
public String pop(String oauth2Id) {
String key = PREFIX + oauth2Id;
String token = redisTemplate.opsForValue().get(key);
if (token != null) {
redisTemplate.delete(key);
}
return token;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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


🏁 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/java

Repository: THIP-TextHip/THIP-Server

Length of output: 1907


임시 refresh token 소비를 원자적으로 바꾸세요. pop에서 GETDELETE를 분리하면 동시 요청이 같은 토큰을 둘 다 가져갈 수 있습니다. 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.

Comment on lines +39 to +53
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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.

Comment on lines +95 to +106
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);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

Comment on lines +42 to +43
@Column(name = "apple_refresh_token", length = 1000)
private String appleRefreshToken;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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에서는 해당
암호문 형식과 암호화된 값의 길이를 수용하도록 컬럼 타입 또는 길이를 변경하세요.

Comment on lines +61 to +65
// 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()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

Comment on lines +48 to +58
// 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);
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

@hd0rable
hd0rable merged commit ba84efc into develop Jul 23, 2026
4 checks passed
@hd0rable
hd0rable deleted the feat/#365-apple-social-login branch July 26, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 애플 소셜로그인 추가

1 participant