Add Apple OAuth components and unify desktop login flow#62
Merged
Conversation
- AppleClientSecretGenerator: ES256 client_secret JWT from .p8 key - AppleTokenClient: authorization code to id_token exchange with connect/read timeouts - ExchangeCodeStore: one-time exchange code store (Caffeine, TTL 120s) - OpaqueTokenGenerator: shared URL-safe token entropy (reused by RefreshTokenHasher) - ErrorCode: INVALID_EXCHANGE_CODE
- GET|POST /api/v1/auth/apple/desktop/callback: exchange code, validate id_token, seal verified subject into one-time exchangeCode, 302 to localhost app - POST /api/v1/auth/apple/desktop/complete: consume exchangeCode and issue tokens - AuthService.signInUp: unify kakao/apple/desktop sign-in into one transactional entry point (member resolution deferred to complete to avoid orphan members)
- oidc.apple: client-id, team-id, key-id, private-key, redirect-uri (injected via moa-secret, empty defaults keep app bootable without them) - gitignore moa-secret/ and *.p8 to prevent committing private keys
Points to submodule commit adding oidc.apple client-id/team-id/key-id/ private-key/redirect-uri used by the desktop login code exchange
Test Results141 tests 141 ✅ 2s ⏱️ Results for commit 56b8ecc. ♻️ This comment has been updated with latest results. |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds Apple OAuth support for a desktop login flow, introducing a one-time exchange-code handoff and refactoring existing sign-in/up logic to reduce duplication while expanding OIDC/Apple configuration and test coverage.
Changes:
- Implemented Apple desktop OAuth callback + completion flow using a one-time exchange code.
- Added Apple OAuth token exchange client and client_secret (JWT) generator, plus required Apple OIDC configuration fields.
- Refactored
AuthServiceto centralize sign-in/up and token issuance; introduced shared opaque token generator used by refresh tokens and exchange codes.
Reviewed changes
Copilot reviewed 18 out of 20 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/resources/application-test.yml | Adds dummy Apple OIDC credentials so test context can boot with the new required Apple config fields. |
| src/test/kotlin/com/moa/controller/AuthControllerTest.kt | Updates controller test wiring for the new AppleDesktopAuthService dependency. |
| src/test/kotlin/com/moa/common/oidc/AppleClientSecretGeneratorTest.kt | Adds unit test validating generated Apple client_secret JWT headers/claims. |
| src/test/kotlin/com/moa/common/auth/ExchangeCodeStoreTest.kt | Adds tests for one-time exchange code issuance/consumption and TTL expiry behavior. |
| src/main/resources/application-prod.yml | Wires Apple OIDC credential secrets into prod config. |
| src/main/resources/application-local.yml | Wires Apple OIDC credential secrets into local config. |
| src/main/kotlin/com/moa/service/dto/DesktopCompleteRequest.kt | Adds DTO for desktop completion endpoint request payload. |
| src/main/kotlin/com/moa/service/AuthService.kt | Refactors to shared signInUp method and shared token issuance. |
| src/main/kotlin/com/moa/service/AppleDesktopAuthService.kt | Introduces service orchestrating Apple desktop callback → exchange code → completion sign-in/up. |
| src/main/kotlin/com/moa/controller/AuthController.kt | Adds Apple desktop callback (GET/POST) and completion endpoints. |
| src/main/kotlin/com/moa/common/oidc/OidcProviderConfig.kt | Expands Apple provider config with required desktop OAuth credentials and token exchange settings. |
| src/main/kotlin/com/moa/common/oidc/AppleTokenClient.kt | Adds Apple token endpoint client with connect/read timeouts and id_token extraction. |
| src/main/kotlin/com/moa/common/oidc/AppleClientSecretGenerator.kt | Adds ES256 JWT client_secret generator parsing PKCS8 EC private key. |
| src/main/kotlin/com/moa/common/exception/ErrorCode.kt | Adds INVALID_EXCHANGE_CODE for invalid/expired one-time code handling. |
| src/main/kotlin/com/moa/common/auth/RefreshTokenHasher.kt | Switches refresh token generation to use OpaqueTokenGenerator. |
| src/main/kotlin/com/moa/common/auth/OpaqueTokenGenerator.kt | Adds shared URL-safe secure random opaque token generator. |
| src/main/kotlin/com/moa/common/auth/ExchangeCodeStore.kt | Adds in-memory (Caffeine) one-time exchange code store with TTL and max-size. |
| build.gradle.kts | Adds Caffeine dependency for the exchange code store. |
| .gitignore | Ignores local secret directory and .p8 key files. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+8
to
+13
| /** | ||
| * OIDC 데스크톱 핸드오프용 1회용 교환 코드 저장소 | ||
| * | ||
| * 단일 인스턴스 배포를 전제로 한 인메모리(Caffeine) 구현이다 | ||
| * — 다중 인스턴스/롤링 배포에서는 외부 저장소로 교체해야 한다. | ||
| */ |
Comment on lines
+32
to
+38
| @Test | ||
| fun `ttl 이 지나면 만료되어 null`() { | ||
| val short = ExchangeCodeStore(ttlSeconds = 1, maxSize = 10) | ||
| val code = short.issue("sub-1") | ||
| Thread.sleep(1100) | ||
| assertThat(short.consume(code)).isNull() | ||
| } |
…eat/desktop-apple-login # Conflicts: # src/main/kotlin/com/moa/common/auth/ExchangeCodeStore.kt
Comment on lines
+47
to
50
| private fun resolveMemberId(provider: ProviderType, subject: String): Long { | ||
| memberRepository.findByProviderAndProviderSubject(provider, subject)?.let { return it.id } | ||
| return memberRepository.save(Member(provider = provider, providerSubject = subject)).id | ||
| } |
Comment on lines
+40
to
+51
| val body = try { | ||
| restClient.post() | ||
| .uri(apple.tokenUri) | ||
| .contentType(MediaType.APPLICATION_FORM_URLENCODED) | ||
| .body(form) | ||
| .retrieve() | ||
| .body(AppleTokenResponse::class.java) | ||
| } catch (ex: Exception) { | ||
| // 원본 예외(네트워크/5xx/4xx/파싱)를 로그로 보존해 진단 가능하게 — 응답은 통일된 INVALID_ID_TOKEN. | ||
| log.warn("Apple token exchange failed", ex) | ||
| throw UnauthorizedException(ErrorCode.INVALID_ID_TOKEN) | ||
| } |
Comment on lines
+28
to
+31
| if (code.isNullOrBlank()) { | ||
| log.warn("Apple desktop callback without code: error={}", appleError) | ||
| return redirect("error", appleError ?: "login_failed", state) | ||
| } |
Comment on lines
+17
to
+23
| # ── 아래 5개는 테스트 전용 더미 값 (컨텍스트 부팅용, 실제 Apple 서비스와 무관) ── | ||
| client-id: test-client-id.do-not-use | ||
| team-id: TESTTEAM00 | ||
| key-id: TESTKEY000 | ||
| # 테스트 전용 일회용 EC P-256 키 — 실서비스 값이 아닌 테스트 값 | ||
| private-key: MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgl4zl/NtEbMusENwEktm8yxGgW7kBI4odCayvCA20sQWhRANCAATTiSGLlOJOUbj+nfVceVSPt7zNMd10H9pQqyctHqxXqnS9FQ3HYDKIBqg3xXT1l6HbtgMAcMScHHOFincw6hx6 | ||
| redirect-uri: https://test.invalid/apple/desktop/callback |
Comment on lines
+54
to
+72
| // GET = 기본(scope 미요청). POST = Apple 이 scope(name/email) 요청 시 강제하는 response_mode=form_post 대비. | ||
| @RequestMapping( | ||
| "/api/v1/auth/apple/desktop/callback", | ||
| method = [RequestMethod.GET, RequestMethod.POST], | ||
| ) | ||
| fun appleCallback( | ||
| @RequestParam(required = false) code: String?, | ||
| @RequestParam(required = false) state: String?, | ||
| @RequestParam(required = false) error: String?, | ||
| ): ResponseEntity<Void> { | ||
| val location = appleDesktopAuthService.callback(code, state, error) | ||
| return ResponseEntity.status(HttpStatus.FOUND).location(URI.create(location)).build() | ||
| } | ||
|
|
||
| @PostMapping("/api/v1/auth/apple/desktop/complete") | ||
| fun appleDesktopComplete( | ||
| @RequestBody @Valid request: DesktopCompleteRequest, | ||
| ): ResponseEntity<ApiResponse<SignInUpResponse>> = | ||
| ResponseEntity.ok(ApiResponse.success(appleDesktopAuthService.complete(request.exchangeCode))) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces a full implementation of Apple OAuth desktop login support, including secure code exchange, improved token handling, and related configuration and tests. The main changes are the addition of a one-time exchange code store, robust Apple OAuth client and secret generation, and a refactor to centralize sign-in/up logic. It also updates configuration to support Apple OAuth credentials and adds comprehensive tests.
Apple OAuth Desktop Login Implementation:
AppleDesktopAuthServiceto handle the Apple OAuth desktop flow, including callback handling, code exchange, and user sign-in/up completion. (src/main/kotlin/com/moa/service/AppleDesktopAuthService.kt, src/main/kotlin/com/moa/service/AppleDesktopAuthService.ktR1-R57)AppleTokenClientandAppleClientSecretGeneratorfor secure communication with Apple OAuth endpoints and client secret JWT generation. (src/main/kotlin/com/moa/common/oidc/AppleTokenClient.kt, [1];src/main/kotlin/com/moa/common/oidc/AppleClientSecretGenerator.kt, [2]Secure One-Time Code Exchange:
ExchangeCodeStorefor issuing and consuming one-time codes, with in-memory storage and TTL, and corresponding tests. (src/main/kotlin/com/moa/common/auth/ExchangeCodeStore.kt, [1];src/test/kotlin/com/moa/common/auth/ExchangeCodeStoreTest.kt, [2]OpaqueTokenGeneratorfor generating secure, URL-safe tokens, used by both refresh tokens and exchange codes. (src/main/kotlin/com/moa/common/auth/OpaqueTokenGenerator.kt, src/main/kotlin/com/moa/common/auth/OpaqueTokenGenerator.ktR1-R15)RefreshTokenHasherto use the newOpaqueTokenGenerator. (src/main/kotlin/com/moa/common/auth/RefreshTokenHasher.kt, src/main/kotlin/com/moa/common/auth/RefreshTokenHasher.ktL5-R9)Controller and DTO Additions:
AuthControllerwith endpoints for Apple desktop callback and completion, and added theDesktopCompleteRequestDTO. (src/main/kotlin/com/moa/controller/AuthController.kt, [1] [2];src/main/kotlin/com/moa/service/dto/DesktopCompleteRequest.kt, [3]Core Auth Logic Refactor:
signInUpmethod inAuthService, reducing duplication and ensuring atomic user creation and token issuance. (src/main/kotlin/com/moa/service/AuthService.kt, src/main/kotlin/com/moa/service/AuthService.ktL30-L101)Configuration and Error Handling:
application-local.ymlandapplication-prod.ymlto support new settings. (src/main/kotlin/com/moa/common/oidc/OidcProviderConfig.kt, [1];src/main/resources/application-local.yml, [2];src/main/resources/application-prod.yml, [3]INVALID_EXCHANGE_CODEfor invalid or expired code handling. (src/main/kotlin/com/moa/common/exception/ErrorCode.kt, src/main/kotlin/com/moa/common/exception/ErrorCode.ktR20)Testing:
ExchangeCodeStoreandAppleClientSecretGenerator. (src/test/kotlin/com/moa/common/auth/ExchangeCodeStoreTest.kt, [1];src/test/kotlin/com/moa/common/oidc/AppleClientSecretGeneratorTest.kt, [2]These changes collectively enable secure and robust Apple OAuth desktop login support, with improved code reuse, error handling, and test coverage.