feature: 애플 로그인 엔드포인트 추가#131
Conversation
- Apple JWKS 설정과 공개키 캐시 provider를 추가 - Apple ID Token 검증과 로그인 API를 추가 - Apple 계정 연결 모델과 마이그레이션을 추가
- Apple audience claim이 문자열과 배열 모두 허용 목록과 비교되도록 보정 - Apple ID Token claim DTO가 audience와 email_verified 타입 변형을 수용하도록 보정 - 신규 Apple 가입 시 Host 저장을 명시
- Apple JWKS provider와 ID Token 검증 테스트를 추가 - Apple 로그인 서비스 흐름과 audience 허용 정책 테스트를 추가
|
Warning Review limit reached
Next review available in: 43 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
Note
|
There was a problem hiding this comment.
Code Review
This pull request implements Apple OAuth login functionality, introducing Apple-specific properties, DTOs, entities, repositories, and updating the authentication service and controller. While the implementation is comprehensive and includes thorough tests, several critical improvements are recommended: handling potential JwtException and null values during token parsing and validation to prevent 500 errors, implementing a fallback mechanism for missing Apple user profiles during registration to avoid blocking users, and adding unique constraints to the host_apple table in the database migration to ensure data integrity.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| private AppleHost toAppleHost(AppleLoginConfirmRequest request) { | ||
| AppleIdToken idToken = jwtParser.parseAppleIdToken(request.idToken(), request.rawNonce()); | ||
| Optional<AppleHost> appleHost = appleHostRepository.findByUserId(idToken.sub()); | ||
| return appleHost.orElseGet(() -> { | ||
| AppleUser appleUser = parseAppleUser(request.user()); | ||
| Host host = new Host(appleUser.fullName(), null, idToken.email()); | ||
| hostRepository.save(host); | ||
| return appleHostRepository.save(new AppleHost(host, idToken.sub())); | ||
| }); | ||
| } | ||
|
|
||
| private AppleUser parseAppleUser(String userJson) { | ||
| if (!StringUtils.hasText(userJson)) { | ||
| throw new BaseException("회원가입을 위해 애플 사용자 정보가 필요합니다.", HttpStatus.BAD_REQUEST); | ||
| } | ||
| try { | ||
| return objectMapper.readValue(userJson, AppleUser.class); | ||
| } catch (JsonProcessingException e) { | ||
| throw new BaseException("애플 사용자 정보 형식이 올바르지 않습니다.", HttpStatus.BAD_REQUEST, e); | ||
| } | ||
| } |
There was a problem hiding this comment.
애플 로그인은 보안 정책상 최초 로그인 시에만 user 객체(이름 등)를 제공하며, 이후 로그인 시에는 user 객체가 전달되지 않습니다.\n\n만약 최초 가입 과정에서 일시적인 오류(예: DB 장애, 네트워크 지연 등)로 인해 가입 처리가 완료되지 않았거나 사용자가 앱을 탈퇴한 후 재가입하려는 경우, request.user()가 null로 들어오게 되어 BaseException이 발생하고 사용자는 영구적으로 가입할 수 없는 상태(블로킹)에 빠지게 됩니다. (사용자가 직접 애플 ID 설정에서 앱 권한을 해제해야만 다시 user를 받을 수 있어 UX에 치명적입니다.)\n\n따라서 신규 가입 시 user 정보가 없더라도 가입을 차단하기보다는, 이메일의 앞부분을 이름으로 사용하거나 기본값(예: 'Apple 사용자')을 부여하여 가입을 성공적으로 완료할 수 있도록 폴백(Fallback) 처리를 구현하는 것을 강력히 권장합니다.
private AppleHost toAppleHost(AppleLoginConfirmRequest request) {
AppleIdToken idToken = jwtParser.parseAppleIdToken(request.idToken(), request.rawNonce());
Optional<AppleHost> appleHost = appleHostRepository.findByUserId(idToken.sub());
return appleHost.orElseGet(() -> {
String name = getAppleHostName(request.user(), idToken.email());
Host host = new Host(name, null, idToken.email());
hostRepository.save(host);
return appleHostRepository.save(new AppleHost(host, idToken.sub()));
});
}
private String getAppleHostName(String userJson, String email) {
if (StringUtils.hasText(userJson)) {
try {
AppleUser appleUser = objectMapper.readValue(userJson, AppleUser.class);
return appleUser.fullName();
} catch (Exception e) {
// JSON 파싱 실패 시 폴백 진행
}
}
if (StringUtils.hasText(email) && email.contains("@")) {
return email.split("@")[0];
}
return "Apple 사용자";
}There was a problem hiding this comment.
탈퇴 시에 애플의 revoke 엔드포인트 요청하므로 탈퇴 후 재가입 시 애플은 다시 최초 유저 정보를 제공함.
일시적 오류로 인한 재시도 시 클라이언트에서 사용자 정보를 보관한 뒤에 다시 요청할 것이므로 제안한 폴백을 반영하지 않을 예정
Test Results 98 files + 3 98 suites +3 1m 10s ⏱️ +4s Results for commit e5fb596. ± Comparison against base commit c3dd913. This pull request removes 14 and adds 30 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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/com/forgather/global/auth/dto/AppleUser.java`:
- Around line 13-18: In AppleUser.fullName(), the BaseException is redundantly
passed HttpStatus.BAD_REQUEST even though BaseException already defaults to 400
when no status is provided. Update the exception throw in fullName() to use the
constructor without the explicit HttpStatus argument, keeping the existing
message and null/blank checks unchanged.
In `@src/main/java/com/forgather/global/auth/model/AppleHost.java`:
- Around line 26-27: `AppleHost.userId` currently only has a non-null column
mapping, so duplicate Apple account records can still be created under
concurrent saves. Add a UNIQUE constraint to the `AppleHost` entity on `userId`
and mirror the same uniqueness in `V21__add_apple_login.sql`; update the mapping
around `AppleHost.userId` so the database enforces one host per Apple account
even when `findByUserId(...)->orElseGet(save)` races.
In `@src/main/java/com/forgather/global/auth/util/JwtParser.java`:
- Around line 81-94: `validateAppleIdToken`에서 `rawNonce`가 null일 때
`hashRawNonce(rawNonce)`가 NPE를 일으키지 않도록, 메서드 초반에 null/blank 검증을 추가해
`JwtParseException`으로 처리하세요. `JwtParser.validateAppleIdToken`와 `hashRawNonce`를
기준으로 수정하고, `AppleLoginConfirmRequest.rawNonce()`가 비어 있으면 `Apple nonce가 올바르지
않습니다.`와 같은 인증 실패로 반환되게 맞추면 됩니다.
- Around line 57-58: `JwtParser.parseClaims` can throw an uncaught NPE when
`idToken` is null because it immediately calls `split("\\.")`; add an explicit
null check at the start of `parseClaims` and fail fast with a handled
`IllegalArgumentException` or the same exception type expected by the caller so
the existing `catch (JsonProcessingException | IllegalArgumentException)` path
can handle it. Use the `parseClaims(String idToken, SocialProvider provider)`
method as the main fix point, and ensure any null `idToken` from request objects
like `AppleLoginConfirmRequest.idToken()` is validated before token parsing
begins.
In `@src/main/resources/db/migration/V21__add_apple_login.sql`:
- Around line 4-13: The `host_apple` table definition in
`V21__add_apple_login.sql` is missing the required uniqueness constraints for
`host_id` and `user_id`, which can allow duplicate mappings and break the
single-row assumption in `findByUserId()` and the `@OneToOne` relationship.
Update the `CREATE TABLE` for `host_apple` (and its constraints, if needed) to
enforce UNIQUE on both `host_id` and `user_id`, using the `host_apple` table
definition and the foreign key setup as the place to apply the fix.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7077436a-8afa-4109-96da-625cf1809928
📒 Files selected for processing (19)
src/main/java/com/forgather/global/auth/client/SocialAuthClient.javasrc/main/java/com/forgather/global/auth/client/SocialProvider.javasrc/main/java/com/forgather/global/auth/controller/AuthController.javasrc/main/java/com/forgather/global/auth/dto/AppleIdToken.javasrc/main/java/com/forgather/global/auth/dto/AppleLoginConfirmRequest.javasrc/main/java/com/forgather/global/auth/dto/AppleUser.javasrc/main/java/com/forgather/global/auth/model/AppleHost.javasrc/main/java/com/forgather/global/auth/model/Host.javasrc/main/java/com/forgather/global/auth/repository/AppleHostRepository.javasrc/main/java/com/forgather/global/auth/service/AuthService.javasrc/main/java/com/forgather/global/auth/util/JwtParser.javasrc/main/java/com/forgather/global/config/AppleProperties.javasrc/main/resources/application.ymlsrc/main/resources/db/migration/V21__add_apple_login.sqlsrc/test/java/com/forgather/global/auth/client/SocialAuthClientTest.javasrc/test/java/com/forgather/global/auth/service/AuthServiceTest.javasrc/test/java/com/forgather/global/auth/util/JwtParserTest.javasrc/test/java/com/forgather/global/config/ApplePropertiesTest.javasrc/test/resources/cleanup.sql
- Apple authorization code를 서버에서 token으로 교환 - Apple ID token 검증 후 refresh token과 회원 연동 정보를 저장 - client secret 생성, 전용 HTTP timeout, 설정 및 스키마 반영
- authorization code 교환 요청과 오류 응답 매핑 검증 - ES256 client secret의 서명과 문자열 audience 검증
- Apple 로그인 요청의 snake_case 매핑과 토큰 교환 오류 처리를 보완 - Apple 설정을 시작 시 검증하고 JWT 검증 실패를 인증 예외로 변환
- invalid_scope 응답의 예외 변환을 검증 - 클라이언트 단위 테스트와 중복되는 서비스 예외 테스트를 정리
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/main/java/com/forgather/global/auth/controller/AuthController.java (1)
69-79: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick winApple 로그인 완료 응답 시 HTTPOnly 쿠키를 설정하도록 수정하세요.
카카오 로그인(
kakaoLoginConfirm)과 동일하게 액세스 토큰과 리프레시 토큰을 HTTPOnly 쿠키에 담아 반환해야 클라이언트 측 인증 세션이 정상적으로 동작합니다. 현재는 응답 바디로만 토큰을 반환하고 있으므로, 쿠키를 함께 설정하는createTokenResponse헬퍼 메서드를 사용하도록 수정해야 합니다.🐛 제안하는 수정
public ResponseEntity<ApiResponse<LoginResponse>> appleLoginConfirm( `@RequestBody` AppleLoginConfirmRequest request ) { var response = authService.appleLoginConfirm(request); - return ResponseEntity.ok(ApiResponse.success(response)); + return createTokenResponse(response); }🤖 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/com/forgather/global/auth/controller/AuthController.java` around lines 69 - 79, Update AuthController.appleLoginConfirm to return the authentication response through the same createTokenResponse helper used by kakaoLoginConfirm, so both access and refresh tokens are set as HTTPOnly cookies while preserving the existing login response payload.src/test/java/com/forgather/global/auth/service/AuthServiceTest.java (1)
142-180: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueApple API의 실제 응답 특성에 맞춰 테스트 케이스를 개선해 보세요. (선택 사항)
기존 가입자가 다시 로그인하는 경우 Apple은
refresh_token을 반환하지 않으므로(null), 테스트에서도null을 반환하도록 설정하고 기존AppleHost의 리프레시 토큰이 덮어쓰이지 않고 온전히 유지되는지 검증하는 것이 실제 프로덕션 동작과 더 일치합니다.💡 제안하는 수정
Host host = new Host("기존사용자", null, "old@example.com"); AppleHost appleHost = new AppleHost(host, "apple-sub", "old-apple-refresh-token"); when(appleAuthClient.exchangeAuthorizationCode("authorization-code")) - .thenReturn(appleTokenResponse("new-apple-refresh-token")); + .thenReturn(appleTokenResponse(null)); when(jwtParser.parseAppleIdToken("apple-server-id-token", "raw-nonce")) .thenReturn(new AppleIdToken(아래 검증 부에서도 기존 토큰이 유지되는지 확인합니다.
verify(appleHostRepository, never()).save(any()); assertThat(host.getEmail()).isEqualTo("old@example.com"); - assertThat(appleHost.getRefreshToken()).isEqualTo("new-apple-refresh-token"); + assertThat(appleHost.getRefreshToken()).isEqualTo("old-apple-refresh-token"); assertThat(response.accessToken()).isEqualTo("access-token");🤖 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/com/forgather/global/auth/service/AuthServiceTest.java` around lines 142 - 180, Update appleLoginConfirm_existingAppleHostDoesNotRequireFullName so exchangeAuthorizationCode returns an Apple token response with a null refresh token, matching Apple’s existing-user login behavior. Keep the existing AppleHost refresh token unchanged and assert that the original token remains after authService.appleLoginConfirm(request).
🤖 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/com/forgather/global/auth/client/AppleAuthClient.java`:
- Around line 42-44: Update the authorizationCode validation in AppleAuthClient
to construct BaseException with only the existing message, removing the
redundant HttpStatus.BAD_REQUEST argument and preserving the current validation
behavior.
In `@src/main/java/com/forgather/global/auth/service/AuthService.java`:
- Around line 90-105: Update toAppleHost so existing AppleHost records only call
updateRefreshToken when appleRefreshToken is non-null and non-empty; preserve
the previously stored token when Apple omits refresh_token during subsequent
logins.
In `@src/main/java/com/forgather/global/auth/util/AppleClientSecretProvider.java`:
- Around line 28-44: Cache the parsed PrivateKey in AppleClientSecretProvider
during bean initialization via a `@PostConstruct` method, parsing the configured
key once and storing it in a field. Update generate() to reuse the cached key in
signWith instead of calling parsePrivateKey() on every invocation.
---
Outside diff comments:
In `@src/main/java/com/forgather/global/auth/controller/AuthController.java`:
- Around line 69-79: Update AuthController.appleLoginConfirm to return the
authentication response through the same createTokenResponse helper used by
kakaoLoginConfirm, so both access and refresh tokens are set as HTTPOnly cookies
while preserving the existing login response payload.
In `@src/test/java/com/forgather/global/auth/service/AuthServiceTest.java`:
- Around line 142-180: Update
appleLoginConfirm_existingAppleHostDoesNotRequireFullName so
exchangeAuthorizationCode returns an Apple token response with a null refresh
token, matching Apple’s existing-user login behavior. Keep the existing
AppleHost refresh token unchanged and assert that the original token remains
after authService.appleLoginConfirm(request).
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a5d88a05-2dc9-49f7-a840-029e78c08e25
📒 Files selected for processing (24)
src/main/java/com/forgather/global/auth/client/AppleAuthClient.javasrc/main/java/com/forgather/global/auth/controller/AuthController.javasrc/main/java/com/forgather/global/auth/dto/AppleLoginConfirmRequest.javasrc/main/java/com/forgather/global/auth/dto/AppleTokenErrorResponse.javasrc/main/java/com/forgather/global/auth/dto/AppleTokenResponse.javasrc/main/java/com/forgather/global/auth/model/AppleHost.javasrc/main/java/com/forgather/global/auth/model/Host.javasrc/main/java/com/forgather/global/auth/service/AuthService.javasrc/main/java/com/forgather/global/auth/util/AppleClientSecretProvider.javasrc/main/java/com/forgather/global/auth/util/JwtParser.javasrc/main/java/com/forgather/global/config/AppleProperties.javasrc/main/java/com/forgather/global/config/RestClientConfig.javasrc/main/resources/application-dev.ymlsrc/main/resources/application-prod.ymlsrc/main/resources/application.ymlsrc/main/resources/db/migration/V22__add_apple_login.sqlsrc/test/java/com/forgather/global/auth/client/AppleAuthClientTest.javasrc/test/java/com/forgather/global/auth/client/SocialAuthClientTest.javasrc/test/java/com/forgather/global/auth/service/AuthServiceTest.javasrc/test/java/com/forgather/global/auth/util/AppleClientSecretProviderTest.javasrc/test/java/com/forgather/global/auth/util/JwtParserTest.javasrc/test/java/com/forgather/global/config/ApplePropertiesTest.javasrc/test/resources/application.ymlsrc/test/resources/cleanup.sql
| @Column(name = "refresh_token", length = 512) | ||
| private String refreshToken; |
There was a problem hiding this comment.
ios 앱 심사를 통과하려면 회원탈퇴 시 애플쪽에 revoke 요청을 보내 연동된 정보를 삭제해야합니다. 그때 refresh_token이 사용되기 때문에 refresh_token을 저장해줍니다.
| public boolean isEmailVerified() { | ||
| return Boolean.TRUE.equals(emailVerified) | ||
| || "true".equalsIgnoreCase(String.valueOf(emailVerified)); |
There was a problem hiding this comment.
apple 공식 문서 상 boolean 타입과 string 타입 중 하나라고 명시되어있어 두 타입 모두 호환 가능하도록 구현했습니다.
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| public record AppleLoginConfirmRequest( |
There was a problem hiding this comment.
애플 네이티브 sdk는 카카오 네이티브 sdk와 달리 access/refresh_token을 발급해주지 않습니다. 따라서 클라이언트에게서 access/refresh_token을 받아오는 카카오와는 다르게 authorization code를 전달 받아 직접 토큰으로 교환하도록 했습니다.
(회원 탈퇴 시 refresh_token 필요)
| .single(APPLE_ISSUER) | ||
| .issuedAt(Date.from(issuedAt)) | ||
| .expiration(Date.from(expiresAt)) | ||
| .signWith(parsePrivateKey(), Jwts.SIG.ES256) |
There was a problem hiding this comment.
애플 개발자 페이지에서 다운 받은 개인키로 서명한 토큰을 애플 로그인 시 client secret으로 활용합니다.
애플은 공개키로 서명을 검증하고 토큰을 교환해줍니다.
| CONSTRAINT `UK_host_apple_host_id` UNIQUE (`host_id`), | ||
| CONSTRAINT `UK_host_apple_user_id` UNIQUE (`user_id`) |
There was a problem hiding this comment.
회원탈퇴, soft delete, 재가입 시 user_id가 중복되어 유니크 제약 조건에 걸릴 수 있습니다.
현재 회원가입 pr에서는 중복 가입 방지 차원에서 user_id에 유니크를 걸어두고 재가입은 회원탈퇴 pr에서 다룰께요!
| private static final Duration APPLE_CONNECT_TIMEOUT = Duration.ofSeconds(3); | ||
| private static final Duration APPLE_READ_TIMEOUT = Duration.ofSeconds(5); |
There was a problem hiding this comment.
초기 구현 단계이므로 너무 짧지도 길지도 않은 수치로 설정했습니다.
특히 인가 코드 -> 토큰 교환 과정에서 한번 소비된 인가 코드는 재사용이 불가하므로 재시도 없이 타임아웃을 여유있게 잡아주었습니다.
| apple: | ||
| jwks-url: http://localhost/apple/auth/keys | ||
| issuer: https://appleid.apple.com | ||
| client-id: test-apple-client-id | ||
| team-id: test-apple-team-id | ||
| key-id: test-apple-key-id | ||
| private-key: test-apple-private-key | ||
| token-url: http://localhost/apple/auth/token |
There was a problem hiding this comment.
실제 application.yml도 이렇게 mock 데이터인데,
이 부분은 애플 개발자 계정 결제 후에 별도 pr에서 채워놓겠습니다.
There was a problem hiding this comment.
이제 git-crypt 말고 별도 .env 파일로 환경 변수 주입하는 방식에 대해 어떻게 생각하시나요?
이전에는 우테코 인프라 제약으로 인해 환경 변수 주입이 까다로워 git-crypt를 선택했던걸로 기억하는데,
아무리 암호화한다고 하더라도 여전히 휴먼에러 등으로 인한 보안 문제가 도사리고 있으며
애플리케이션 실행 시점에 .env 파일로 주입하는게 더 유연하다고 생각합니다.
관리 포인트도 저희 둘 로컬과 서버뿐이기도 하고요.
| private AppleHost toAppleHost(AppleLoginConfirmRequest request) { | ||
| AppleIdToken idToken = jwtParser.parseAppleIdToken(request.idToken(), request.rawNonce()); | ||
| Optional<AppleHost> appleHost = appleHostRepository.findByUserId(idToken.sub()); | ||
| return appleHost.orElseGet(() -> { | ||
| AppleUser appleUser = parseAppleUser(request.user()); | ||
| Host host = new Host(appleUser.fullName(), null, idToken.email()); | ||
| hostRepository.save(host); | ||
| return appleHostRepository.save(new AppleHost(host, idToken.sub())); | ||
| }); | ||
| } | ||
|
|
||
| private AppleUser parseAppleUser(String userJson) { | ||
| if (!StringUtils.hasText(userJson)) { | ||
| throw new BaseException("회원가입을 위해 애플 사용자 정보가 필요합니다.", HttpStatus.BAD_REQUEST); | ||
| } | ||
| try { | ||
| return objectMapper.readValue(userJson, AppleUser.class); | ||
| } catch (JsonProcessingException e) { | ||
| throw new BaseException("애플 사용자 정보 형식이 올바르지 않습니다.", HttpStatus.BAD_REQUEST, e); | ||
| } | ||
| } |
There was a problem hiding this comment.
탈퇴 시에 애플의 revoke 엔드포인트 요청하므로 탈퇴 후 재가입 시 애플은 다시 최초 유저 정보를 제공함.
일시적 오류로 인한 재시도 시 클라이언트에서 사용자 정보를 보관한 뒤에 다시 요청할 것이므로 제안한 폴백을 반영하지 않을 예정
- Provider 생성 시 private key 형식을 검증하고 파싱 결과를 재사용 - 잘못된 private key 입력에 대한 예외 테스트 추가
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/test/resources/application.yml (1)
78-91: 📐 Maintainability & Code Quality | 🔵 Trivial
git-crypt대신.env및 환경 변수를 활용하는 방식에 적극 찬성합니다.과거 코멘트에서 질문해 주신 환경 변수 관리 방식 변경에 대해 답변드립니다.
말씀하신 것처럼git-crypt는 파일 자체가 암호화되더라도 복호화 키 관리 문제나 휴먼 에러로 인한 시크릿 유출 위험이 지속적으로 존재합니다.
.env파일이나 런타임 환경 변수로 설정값을 주입하는 방식(12-Factor App 방법론)은 코드와 설정을 완전히 분리하므로 다음과 같은 이점이 있습니다:
- 로컬, 테스트, 운영 등 각 환경 간 유연한 전환 및 개별 설정 관리 용이
- CI/CD 파이프라인(예: GitHub Actions Secrets)과의 안전하고 원활한 연동
- 시크릿 값만 변경될 때 불필요한 코드 커밋 생성 방지
현재 관리 포인트가 서버와 로컬 환경 정도로 단순하다면, 말씀하신 대로
.env방식을 도입하는 것이 관리의 유연성과 보안 측면에서 더 나은 방향이 될 것입니다. 추후 환경 변수 주입 방식으로 전환하는 리팩토링 진행을 권장합니다.(참고: 정적 분석 도구에서 해당 파일 내의 Private Key를 감지하여 보안 경고를 발생시킬 수 있으나, 주석에 명시된 바와 같이 테스트 전용 Mock 데이터이므로 무시하셔도 무방합니다.)
🤖 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/resources/application.yml` around lines 78 - 91, application.yml의 apple.private-key에 하드코딩된 테스트 개인 키를 제거하고 .env 또는 런타임 환경 변수에서 주입되도록 변경하세요. Apple 설정의 private-key 참조가 환경 변수 값을 사용하도록 수정하고, 기존 테스트 설정에서 해당 값이 정상적으로 로드되도록 구성하세요.
🤖 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.
Outside diff comments:
In `@src/test/resources/application.yml`:
- Around line 78-91: application.yml의 apple.private-key에 하드코딩된 테스트 개인 키를 제거하고
.env 또는 런타임 환경 변수에서 주입되도록 변경하세요. Apple 설정의 private-key 참조가 환경 변수 값을 사용하도록 수정하고,
기존 테스트 설정에서 해당 값이 정상적으로 로드되도록 구성하세요.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: acb3fd9a-a16e-41b5-b253-ea7d6fc00f81
📒 Files selected for processing (6)
src/main/java/com/forgather/global/auth/util/AppleClientSecretProvider.javasrc/main/resources/application-dev.ymlsrc/main/resources/application-prod.ymlsrc/main/resources/application.ymlsrc/test/java/com/forgather/global/auth/util/AppleClientSecretProviderTest.javasrc/test/resources/application.yml
- 다른 로그인 API와 동일하게 토큰을 응답 바디와 HttpOnly 쿠키로 반환
연관된 이슈
변경 사항
POST /auth/login/apple/confirmApple 로그인 완료 API를 추가했습니다.host_apple테이블 및 호스트 이메일 컬럼을 추가했습니다.작업 내용
API
POST /auth/login/apple/confirmApple 네이티브 로그인에서 발급받은 authorization code를 서버가 Apple token endpoint에 교환하고, 검증된 Apple 사용자 정보로 로그인합니다.
로그인에 성공하면 Forgather access token과 refresh token을 응답 바디와 HttpOnly 쿠키로 함께 반환합니다.
클라이언트가 전달한
id_token은 요청 호환을 위해 받지만 인증에는 사용하지 않습니다. 실제 인증과 회원 식별에는 Apple code 교환 응답의 ID token을 사용합니다.[요청]
id_tokenauthorization_coderaw_noncefull_name[요청 예시]
{ "id_token": "client-issued-apple-id-token", "authorization_code": "client-issued-authorization-code", "raw_nonce": "client-generated-raw-nonce", "full_name": "홍길동" }[응답]
[응답 예시 - 200]
{ "code": "SUCCESS", "message": null, "data": { "accessToken": "forgather-access-token", "refreshToken": "forgather-refresh-token" } }[응답 쿠키]
access_token/refresh_token/auth/refresh기존 로그인 및 토큰 재발급 API와 동일한
AuthCookieProvider정책을 적용합니다.Apple authorization code 교환
[인증 기준]
모든 Apple 로그인에서 authorization code를 Apple
/auth/token에 교환합니다.클라이언트가 전달한 ID token으로 code 교환 실패를 우회하지 않으며, Apple 서버가 반환한 ID token만 검증해 사용자를 식별합니다.
[AppleAuthClient]
Apple token endpoint에
application/x-www-form-urlencoded형식으로 다음 값을 전달합니다.client_idclient_secretcodegrant_type=authorization_code응답의 access token, refresh token, ID token 및 만료 시간이 모두 존재하는지 확인합니다. Apple 오류 응답은
invalid_request,invalid_grant,invalid_client등의 유형에 따라 400, 401 또는 502 응답으로 변환합니다.[HTTP timeout]
Apple 외부 API 호출이 무기한 대기하지 않도록 전용
RestClient에 connect timeout 3초와 read timeout 5초를 적용했습니다.Apple client secret
[ES256 서명]
Apple Team ID, Client ID, Key ID와
.p8private key를 사용해 5분 동안 유효한 client secret JWT를 ES256으로 생성합니다.[초기화 검증]
애플리케이션 초기화 시 private key 형식을 검증하고 파싱한 키를 재사용합니다. 잘못된 private key 설정은 실제 로그인 요청 시점이 아니라 Provider 생성 시점에 확인됩니다.
private key와 생성된 client secret은 로그에 기록하지 않습니다.
Apple ID token 검증
[공개키 조회]
SocialProvider에APPLE을 추가하고,SocialAuthClient가 Apple JWKS URL의 공개키를 provider별로 조회·캐시하도록 확장했습니다.[검증 항목]
Apple token 교환 응답의 ID token에 대해 다음 항목을 검증합니다.
iss가 Apple issuer와 일치하는지aud가 설정된 Apple Client ID와 일치하는지sub와email이 존재하는지email_verified가 boolean 또는 문자열true인지SHA-256(raw_nonce)와 일치하는지audience는 문자열과 배열 형식을 모두 지원합니다.
Apple 회원 로그인 처리
[신규 회원]
검증된 ID token의
sub에 연결된 Apple 계정이 없으면 다음 정보를 저장합니다.Host.name: 클라이언트가 전달한full_nameHost.email: 검증된 Apple ID token의 emailHost.pictureUrl:nullAppleHost.userId: 검증된 Apple ID token의subAppleHost.refreshToken: Apple code 교환 응답의 refresh token신규 회원에게
full_name이 없거나 공백이면 로그인을 거부합니다.[기존 회원]
기존 Apple 회원은
sub로 조회하며, 요청의 이름이나 ID token의 이메일로 기존 Host 정보를 갱신하지 않습니다.Apple code 교환에서 새로 받은 refresh token만 저장한 뒤 기존 Host로 로그인합니다.
[Forgather 토큰]
회원 확인 또는 생성이 완료되면 Forgather access token과 refresh token을 발급해 기존
LoginResponse응답 바디와 HttpOnly 쿠키에 함께 담아 반환합니다.Apple access token 저장, Apple 계정과 다른 소셜 계정의 자동 연결, 회원 탈퇴 시 Apple token revoke는 이번 작업 범위에 포함하지 않았습니다.
DB 마이그레이션 및 설정
[host]
Apple ID token에서 확인한 이메일을 저장할 수 있도록
host.email컬럼을 추가했습니다.[host_apple]
V22__add_apple_login.sql에 Apple 계정 연동 테이블을 추가했습니다.host_iduser_idrefresh_tokenhost_id,user_idunique 제약host테이블 외래 키[환경 설정]
환경별 설정에 다음 Apple 로그인 값을 추가했습니다.
Summary by CodeRabbit