Skip to content

Add Apple OAuth components and unify desktop login flow#62

Merged
subsub97 merged 10 commits into
mainfrom
feat/desktop-apple-login
Jul 6, 2026
Merged

Add Apple OAuth components and unify desktop login flow#62
subsub97 merged 10 commits into
mainfrom
feat/desktop-apple-login

Conversation

@subsub97

@subsub97 subsub97 commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

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:

  • Added AppleDesktopAuthService to 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)
  • Implemented AppleTokenClient and AppleClientSecretGenerator for 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:

  • Added ExchangeCodeStore for 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]
  • Added OpaqueTokenGenerator for 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)
  • Updated RefreshTokenHasher to use the new OpaqueTokenGenerator. (src/main/kotlin/com/moa/common/auth/RefreshTokenHasher.kt, src/main/kotlin/com/moa/common/auth/RefreshTokenHasher.ktL5-R9)

Controller and DTO Additions:

  • Updated AuthController with endpoints for Apple desktop callback and completion, and added the DesktopCompleteRequest DTO. (src/main/kotlin/com/moa/controller/AuthController.kt, [1] [2]; src/main/kotlin/com/moa/service/dto/DesktopCompleteRequest.kt, [3]

Core Auth Logic Refactor:

Configuration and Error Handling:

  • Extended Apple provider config to include all required OAuth credentials; updated application-local.yml and application-prod.yml to 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]
  • Added new error code INVALID_EXCHANGE_CODE for invalid or expired code handling. (src/main/kotlin/com/moa/common/exception/ErrorCode.kt, src/main/kotlin/com/moa/common/exception/ErrorCode.ktR20)

Testing:

  • Added unit tests for ExchangeCodeStore and AppleClientSecretGenerator. (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.

subsub97 added 5 commits July 6, 2026 07:35
- 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
Copilot AI review requested due to automatic review settings July 5, 2026 23:35
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Test Results

141 tests   141 ✅  2s ⏱️
 26 suites    0 💤
 26 files      0 ❌

Results for commit 56b8ecc.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 AuthService to 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()
}

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 22 changed files in this pull request and generated 5 comments.

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)))
@subsub97 subsub97 merged commit d3a879a into main Jul 6, 2026
3 checks passed
@subsub97 subsub97 deleted the feat/desktop-apple-login branch July 6, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants