feat: Apple 로그인 서버 인증 및 토큰 자동 리프레시 구현 - #79
Conversation
- HttpClientModule에 Bearer Auth 플러그인 설치 (loadTokens, refreshTokens, sendWithoutRequest) - 별도 refreshClient로 토큰 갱신 API 호출 (순환 참조 방지) - AuthEventBus(SharedFlow)로 리프레시 실패 시 인증 만료 이벤트 전파 - MockEngine 기반 통합 테스트 6개 시나리오 (토큰 첨부, 제외 경로, 리프레시+재시도, 실패 처리, 동시 요청) - ktor-client-auth, ktor-client-mock 의존성 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- signIn API에 provider 파라미터 추가하여 APPLE/KAKAO 로그인 구분 - AppleLoginUseCase에 서버 인증 요청 및 토큰 저장 로직 추가 - iOS Apple Sign-In에 nonce 생성(UUID) 및 SHA256 해시 처리 적용 - AppleSignInResult에 nonce 필드 추가 - LandingViewModel에서 실제 Apple 로그인 플로우 연결 - Apple 로그인 버튼에 로딩 상태 반영 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WalkthroughApple/Kakao 로그인에 provider/nonce 전달을 추가하고, HTTP 클라이언트에 토큰 자동 갱신 및 AuthEventBus를 도입했으며, iOS nonce 생성과 토큰 인증 통합 테스트 및 관련 DI/뷰모델·UI 변경을 추가했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as "User"
participant UI as "LandingScreen"
participant VM as "LandingViewModel"
participant UseCase as "AppleLoginUseCase"
participant AppleClient as "AppleSignInClient"
participant Repo as "AuthRepository"
participant Remote as "RemoteAuthDataSource"
participant HTTP as "HttpClient"
participant Refresh as "refreshClient"
participant Server as "Backend API"
participant Bus as "AuthEventBus"
User->>UI: Apple 로그인 클릭
UI->>VM: onAppleLogin()
VM->>VM: isLoading = true
VM->>UseCase: invoke()
UseCase->>AppleClient: signIn() (identityToken, nonce)
AppleClient-->>UseCase: AppleSignInResult(identityToken, nonce)
UseCase->>Repo: signIn(provider="APPLE", idToken, nonce)
Repo->>Remote: signIn(provider, idToken, nonce)
Remote->>HTTP: POST users/signin (payload 포함)
HTTP->>Server: 요청 (Bearer 헤더 자동 첨부 가능)
alt 200
Server-->>HTTP: accessToken, refreshToken
HTTP->>Repo: 응답 전달
else 401
HTTP->>Refresh: POST auth/refresh (refreshToken)
Refresh->>Server: refresh 요청
alt refresh success
Server-->>Refresh: 새 accessToken, refreshToken
Refresh->>HTTP: 반환된 토큰 전달
HTTP->>Repo: 토큰 저장 및 원래 요청 재시도
else refresh fail
HTTP->>Bus: emit(TokenExpired)
HTTP->>Repo: clearTokens
end
end
Repo-->>UseCase: SignInResult
UseCase-->>VM: Success -> NavigateHome
VM->>VM: isLoading = false
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 분 Possibly related PRs
Suggested reviewers
시
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/MainViewModel.kt (1)
11-11:⚠️ Potential issue | 🟡 Minor미사용 import 제거 필요
UnknownException은 파일에서 참조되지 않으므로 import를 제거하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/MainViewModel.kt` at line 11, Remove the unused import of UnknownException from the top of MainViewModel (delete the line importing com.chukchukhaksa.mobile.common.model.UnknownException) so there are no unused imports; if the exception is intended to be used, instead reference UnknownException where needed (e.g., in error handling methods) or add the correct usage, otherwise simply remove the import line.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/auth/repository/AuthRepository.kt`:
- Around line 5-6: Change the provider parameter from a free-form String to a
type-safe enum/sealed type and update usages: replace AuthRepository.suspend fun
signIn(provider: String, idToken: String, nonce: String): SignInResult with a
version that accepts an AuthProvider (or sealed class) and create the
AuthProvider enum (e.g., APPLE, KAKAO) so callers use AuthProvider.KAKAO; then
update all call sites of AuthRepository.signIn to pass the new enum value and
adjust any serialization/mapping code where provider strings are
produced/consumed.
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/auth/usecase/AppleLoginUseCase.kt`:
- Line 15: Extract the hard-coded provider string into a shared constant: create
an object (e.g., AuthProvider) with const val APPLE = "APPLE" and KAKAO =
"KAKAO", then replace provider = "APPLE" in AppleLoginUseCase and provider =
"KAKAO" in KakaoLoginUseCase to use AuthProvider.APPLE and AuthProvider.KAKAO
respectively to ensure consistency and avoid magic strings.
In `@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/MainViewModel.kt`:
- Around line 69-71: Current code always shows "네트워크 오류가 발생했습니다." then calls
throwable.record(); update the error handling in MainViewModel (the block that
calls onShowToast and throwable.record()) to inspect the exception type (e.g.,
check for NetworkException) and choose a message accordingly — show a specific
network message only for NetworkException, map other known exceptions
(auth/validation) to appropriate localized messages, and otherwise show a
generic fallback like "오류가 발생했습니다."; still call throwable.record() after
deciding the message.
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/auth/AuthEventBus.kt`:
- Around line 7-13: AuthEventBus currently constructs _events as
MutableSharedFlow<AuthEvent>(extraBufferCapacity = 1) so events emitted when
there are no subscribers are lost; update the MutableSharedFlow construction in
class AuthEventBus (the _events property) to include replay = 1 (e.g.,
MutableSharedFlow<AuthEvent>(replay = 1, extraBufferCapacity = 1)) so that late
subscribers to events (AuthEventBus.events) will receive the last emitted event;
leave the emit() method (tryEmit or semantics) unchanged.
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt`:
- Around line 104-110: The code calls localAuthDataSource.clearTokens() when
currentRefreshToken == null inside refreshTokens, which will also remove the
access token and can be unexpected; change this to only clear the refresh token
(e.g., call a clearRefreshToken()/removeRefreshToken() method) or remove the
clearTokens() call if you want to preserve the access token, but keep
authEventBus.emit() and the return@refreshTokens null behavior in the
refreshTokens block so the missing-refresh-token case is handled without
deleting unrelated tokens.
- Around line 61-74: The refreshClient HttpClient is missing timeout
configuration so refresh token requests may hang; add the same HttpTimeout
install used in the main client to refreshClient (install(HttpTimeout) with
appropriate connect/request/socket timeouts) and ensure it is configured
alongside the existing ContentNegotiation and defaultRequest settings on the
refreshClient declaration so refresh calls use the same timeout behavior as the
main HttpClient.
In
`@composeApp/src/commonTest/kotlin/com/chukchukhaksa/mobile/remote/auth/TokenAuthIntegrationTest.kt`:
- Around line 49-52: TokenAuthIntegrationTest.kt defines AUTH_EXCLUDED_PATHS
that duplicates the list in HttpClientModule; remove the duplicated list from
the test and instead reference the single source of truth by importing or
accessing the constant from HttpClientModule (or extract a new shared constant
in a common test/production constants object and use that in both places) so the
test stays in sync with production paths.
- Around line 306-344: The test `동시 요청 시 리프레시는 1회만 수행된다` relies on unreliable
Ktor behavior; update the test to either (A) enforce synchronization inside the
refresh flow by making the client’s `refreshTokens` callback use a Mutex (so
concurrent 401s will queue and only one actual refresh HTTP call is made —
reference the function/callback used when building the client in
`buildTestClient` and the refresh engine `refreshEngine`), or (B) relax the
assertion to not require exactly one refresh and instead assert functional
outcomes only (e.g., that all requests eventually succeed and that
`FakeLocalAuthDataSource`’s tokens were updated to
`"new-access-token"`/`"new-refresh-token"`), removing the brittle
`assertEquals(1, refreshLog.size)` check; pick one approach and apply it to the
test and any refresh-callback implementation used by `buildTestClient`.
In
`@composeApp/src/iosMain/kotlin/com/chukchukhaksa/mobile/common/kmp/AppleSignInClient.ios.kt`:
- Around line 35-36: Replace the ad-hoc sha256Hex(rawNonce) call in
AppleSignInClient.ios.kt with a call to a vetted crypto library: add either
org.kotlincrypto.hash:sha2 (pure Kotlin KMP) or cryptography-kotlin (delegates
to CryptoKit on iOS), implement a small wrapper function (e.g., computeSha256Hex
or reuse sha256Hex name) that uses the library API to produce the hex digest
from rawNonce, update imports and platform-specific implementation in
AppleSignInClient (and tests) to call that wrapper instead of the custom
implementation, and ensure error handling and test coverage remain intact.
---
Outside diff comments:
In `@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/MainViewModel.kt`:
- Line 11: Remove the unused import of UnknownException from the top of
MainViewModel (delete the line importing
com.chukchukhaksa.mobile.common.model.UnknownException) so there are no unused
imports; if the exception is intended to be used, instead reference
UnknownException where needed (e.g., in error handling methods) or add the
correct usage, otherwise simply remove the import line.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: 21d636f2-2ccf-474d-8cda-c6f4395374ec
📒 Files selected for processing (19)
composeApp/build.gradle.ktscomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/MainViewModel.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/common/kmp/AppleSignInClient.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/data/auth/datasource/RemoteAuthDataSource.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/data/auth/repository/AuthRepositoryImpl.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/di/DomainModules.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/auth/repository/AuthRepository.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/auth/usecase/AppleLoginUseCase.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/auth/usecase/KakaoLoginUseCase.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingScreen.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingViewModel.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/auth/AuthEventBus.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/auth/RemoteAuthDataSourceImpl.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/auth/model/SignInRequest.ktcomposeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.ktcomposeApp/src/commonTest/kotlin/com/chukchukhaksa/mobile/remote/auth/FakeLocalAuthDataSource.ktcomposeApp/src/commonTest/kotlin/com/chukchukhaksa/mobile/remote/auth/TokenAuthIntegrationTest.ktcomposeApp/src/iosMain/kotlin/com/chukchukhaksa/mobile/common/kmp/AppleSignInClient.ios.ktgradle/libs.versions.toml
- refreshClient에 timeout 설정이 없어 네트워크 문제 시 무한 대기 가능한 문제 수정 - 테스트에서 AUTH_EXCLUDED_PATHS를 중복 정의하지 않고 프로덕션 상수를 참조하도록 변경 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt (1)
109-130:⚠️ Potential issue | 🟠 Major
refreshTokens는 직렬화하고CancellationException은 재던지세요.Ktor 공식 API는
refreshTokens를 401 수신 시 실행되는 콜백으로만 설명하고, 3.1 changelog에도 401 응답이 이 콜백으로 올바르게 큐잉되지 않는 문제가 언급돼 있습니다. 지금처럼 별도 동기화 없이 refresh를 수행하고catch (Exception)로 전부 만료 처리하면, 동시 만료 요청에서 refresh token을 회전시키는 서버와 경합할 수 있고 취소/timeout에도clearTokens()+emit()로 사용자를 로그아웃시킬 수 있습니다.Mutex로 refresh를 1회만 수행하고,CancellationException은 재던지며, 토큰 삭제는 서버가 refresh token 무효를 명시한 경우로 한정해 주세요. (api.ktor.io)Ktor client Bearer Auth의 `refreshTokens`는 동시 401 응답을 1회의 refresh로 보장하나요? 또 Kotlin coroutine 문서는 `catch (Exception)`에서 `CancellationException`을 재던지라고 안내하나요?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt` around lines 109 - 130, The refreshTokens block currently catches all Exceptions, clears tokens and emits logout, and may run multiple concurrent refreshes; change it to use a coroutine-safe single-flight Mutex (e.g., tokenRefreshMutex) to ensure only one refreshClient.post(...) runs at a time, rethrow CancellationException immediately, and only call localAuthDataSource.clearTokens() + authEventBus.emit() when the refresh response explicitly indicates the refresh token is invalid/expired (e.g., check response status or API error code), otherwise propagate or return null without wiping local tokens; keep refreshClient.post(...) and localAuthDataSource.saveAccessToken/saveRefreshToken as before but perform these updates inside the mutex-protected successful-path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt`:
- Around line 134-138: 현재 sendWithoutRequest 블록에서 requestPath를
AUTH_EXCLUDED_PATHS와 부분 문자열 비교(contains)하여 오탐이 발생합니다;
request.url.pathSegments.joinToString("/")로 만든 상대 경로를 정규화(불필요한 앞/뒤 슬래시 제거 및 URL
디코딩 등)한 뒤 AUTH_EXCLUDED_PATHS의 각 항목과 정확한 문자열 비교(==)로 검사하도록 바꾸세요; 대상 식별자는
sendWithoutRequest, requestPath, AUTH_EXCLUDED_PATHS 입니다.
In
`@composeApp/src/commonTest/kotlin/com/chukchukhaksa/mobile/remote/auth/TokenAuthIntegrationTest.kt`:
- Around line 79-146: The test reimplements production auth logic in
buildTestClient; extract the shared HttpClient builder from HttpClientModule
into a reusable function (e.g., createAuthHttpClient or HttpClientFactory) that
accepts dependencies like a LocalAuthDataSource, AuthEventBus and an Engine,
then update both HttpClientModule and the test to call that shared builder
instead of duplicating loadTokens, refreshTokens and sendWithoutRequest logic;
ensure the new factory wires the same loadTokens, refreshTokens and
sendWithoutRequest implementations used in production so tests exercise the real
auth behavior.
---
Duplicate comments:
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt`:
- Around line 109-130: The refreshTokens block currently catches all Exceptions,
clears tokens and emits logout, and may run multiple concurrent refreshes;
change it to use a coroutine-safe single-flight Mutex (e.g., tokenRefreshMutex)
to ensure only one refreshClient.post(...) runs at a time, rethrow
CancellationException immediately, and only call
localAuthDataSource.clearTokens() + authEventBus.emit() when the refresh
response explicitly indicates the refresh token is invalid/expired (e.g., check
response status or API error code), otherwise propagate or return null without
wiping local tokens; keep refreshClient.post(...) and
localAuthDataSource.saveAccessToken/saveRefreshToken as before but perform these
updates inside the mutex-protected successful-path.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: c1a99b62-57f2-4027-b98e-8c80f35f0bb7
📒 Files selected for processing (2)
composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.ktcomposeApp/src/commonTest/kotlin/com/chukchukhaksa/mobile/remote/auth/TokenAuthIntegrationTest.kt
| sendWithoutRequest { request -> | ||
| val requestPath = request.url.pathSegments.joinToString("/") | ||
| AUTH_EXCLUDED_PATHS.none { path -> | ||
| requestPath.contains(path) | ||
| } |
There was a problem hiding this comment.
인증 제외 경로는 부분 문자열이 아니라 정확히 비교하세요.
contains()라서 users/signin-extra나 auth/refresh-preview 같은 경로도 제외 대상으로 처리됩니다. 상대 경로를 정규화한 뒤 AUTH_EXCLUDED_PATHS와 정확히 비교하는 편이 안전합니다.
🛠️ 제안
sendWithoutRequest { request ->
- val requestPath = request.url.pathSegments.joinToString("/")
- AUTH_EXCLUDED_PATHS.none { path ->
- requestPath.contains(path)
- }
+ val requestPath = request.url.pathSegments
+ .filter { it.isNotBlank() }
+ .dropWhile { it == "api" }
+ .joinToString("/")
+ requestPath !in AUTH_EXCLUDED_PATHS
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt`
around lines 134 - 138, 현재 sendWithoutRequest 블록에서 requestPath를
AUTH_EXCLUDED_PATHS와 부분 문자열 비교(contains)하여 오탐이 발생합니다;
request.url.pathSegments.joinToString("/")로 만든 상대 경로를 정규화(불필요한 앞/뒤 슬래시 제거 및 URL
디코딩 등)한 뒤 AUTH_EXCLUDED_PATHS의 각 항목과 정확한 문자열 비교(==)로 검사하도록 바꾸세요; 대상 식별자는
sendWithoutRequest, requestPath, AUTH_EXCLUDED_PATHS 입니다.
AuthConfig.configureBearerAuth() 확장 함수로 loadTokens, refreshTokens, sendWithoutRequest 로직을 추출하고, HttpClientModule과 TokenAuthIntegrationTest 양쪽에서 공유하도록 변경 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt (2)
96-100:⚠️ Potential issue | 🟡 Minor인증 제외 경로는 부분 문자열이 아니라 정확 비교로 검사하세요.
지금처럼
contains()를 쓰면users/signin-extra나auth/refresh-preview같은 경로도 제외 대상으로 오인됩니다. 경로를 정규화한 뒤AUTH_EXCLUDED_PATHS와 정확히 비교해야 의도한 엔드포인트만 우회됩니다.🛠️ 제안
sendWithoutRequest { request -> - val requestPath = request.url.pathSegments.joinToString("/") - AUTH_EXCLUDED_PATHS.none { path -> - requestPath.contains(path) - } + val requestPath = request.url.pathSegments + .filter { it.isNotBlank() } + .joinToString("/") + .removePrefix("api/") + requestPath !in AUTH_EXCLUDED_PATHS }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt` around lines 96 - 100, 현재 sendWithoutRequest 블록에서 requestPath를 AUTH_EXCLUDED_PATHS와 contains()로 부분 문자열 검사하고 있어 의도치 않은 매칭이 발생합니다; requestPath를 정규화(중복/선행/후행 슬래시 제거, URL 디코딩 등)한 뒤 AUTH_EXCLUDED_PATHS의 각 항목과 정확(equal) 비교하도록 변경하세요 (즉 sendWithoutRequest 내부의 requestPath 계산과 AUTH_EXCLUDED_PATHS.none { ... } 로직을 수정하여 contains 대신 == 또는 정확한 경로 매칭 로직을 사용하고 필요하면 세그먼트 단위 비교를 적용).
71-87:⚠️ Potential issue | 🟠 Major동시 401에서 refresh 경합을 막는 직렬화가 없습니다.
현재 구현은 refresh 구간을 로컬에서 보호하지 않아서, bearer 플러그인이 동시 401에 대해
refreshTokens를 여러 번 호출하는 버전/상황에서는 같은 refresh token으로 중복 갱신이 발생할 수 있습니다. 토큰 회전 서버라면 뒤늦은 refresh 실패가 방금 저장한 새 토큰 상태를 다시 만료 처리하는 경로로 이어질 수 있으니, single-refresh가 요구사항이면Mutex로 이 블록을 serialize하고 잠금 획득 후 저장된 토큰을 재확인하는 편이 안전합니다.이 저장소가 사용하는 Ktor client 버전에서 bearer Auth 플러그인의 `refreshTokens {}` 는 동시에 여러 요청이 `401 Unauthorized`를 받았을 때 refresh 호출을 1회로 직렬화해 주나요, 아니면 애플리케이션이 `Mutex` 등으로 직접 보호해야 하나요?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt` around lines 71 - 87, Wrap the refresh flow in a Mutex to serialize concurrent refreshes: introduce a shared Mutex (e.g., refreshMutex) and inside the refreshTokens { } handler call refreshMutex.withLock { ... }; after acquiring the lock re-read the stored refresh token via localAuthDataSource.getRefreshToken() to detect if another coroutine already refreshed and return the current tokens if so, otherwise perform the refresh POST (the existing refreshClient.post("auth/refresh") / response.getDataOrThrow()), then save with localAuthDataSource.saveAccessToken(...) and saveRefreshToken(...), and return the new BearerTokens(result.accessToken, result.refreshToken); ensure the branch that handles null tokens still clears tokens and emits authEventBus.emit() before returning null.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt`:
- Around line 88-92: The catch block in HttpClientModule.kt currently treats any
Exception as a guaranteed token-invalidation case and calls
localAuthDataSource.clearTokens() and authEventBus.emit(), which disconnects
users on transient failures; change the handler in the refresh flow (the catch
around the refresh call) to only clear tokens and emit logout when the exception
is a server response that definitively indicates an invalid/expired refresh
token (e.g., inspect ResponseException / HttpResponseException and check
response.status is the specific status the backend uses for invalid refresh
tokens like 401/403 or a dedicated error code), otherwise preserve tokens and
propagate or return a failed refresh result without calling
localAuthDataSource.clearTokens() or authEventBus.emit().
---
Duplicate comments:
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt`:
- Around line 96-100: 현재 sendWithoutRequest 블록에서 requestPath를
AUTH_EXCLUDED_PATHS와 contains()로 부분 문자열 검사하고 있어 의도치 않은 매칭이 발생합니다; requestPath를
정규화(중복/선행/후행 슬래시 제거, URL 디코딩 등)한 뒤 AUTH_EXCLUDED_PATHS의 각 항목과 정확(equal) 비교하도록
변경하세요 (즉 sendWithoutRequest 내부의 requestPath 계산과 AUTH_EXCLUDED_PATHS.none { ... }
로직을 수정하여 contains 대신 == 또는 정확한 경로 매칭 로직을 사용하고 필요하면 세그먼트 단위 비교를 적용).
- Around line 71-87: Wrap the refresh flow in a Mutex to serialize concurrent
refreshes: introduce a shared Mutex (e.g., refreshMutex) and inside the
refreshTokens { } handler call refreshMutex.withLock { ... }; after acquiring
the lock re-read the stored refresh token via
localAuthDataSource.getRefreshToken() to detect if another coroutine already
refreshed and return the current tokens if so, otherwise perform the refresh
POST (the existing refreshClient.post("auth/refresh") /
response.getDataOrThrow()), then save with
localAuthDataSource.saveAccessToken(...) and saveRefreshToken(...), and return
the new BearerTokens(result.accessToken, result.refreshToken); ensure the branch
that handles null tokens still clears tokens and emits authEventBus.emit()
before returning null.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: ff69406a-2d18-499a-876b-15ca1db33d5a
📒 Files selected for processing (2)
composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.ktcomposeApp/src/commonTest/kotlin/com/chukchukhaksa/mobile/remote/auth/TokenAuthIntegrationTest.kt
| } catch (e: Exception) { | ||
| Napier.e("Token refresh failed", e) | ||
| localAuthDataSource.clearTokens() | ||
| authEventBus.emit() | ||
| null |
There was a problem hiding this comment.
일시적 refresh 실패까지 로그아웃 처리하면 세션이 불필요하게 끊깁니다.
여기서는 catch (Exception)이 타임아웃, 네트워크 단절, 5xx, 역직렬화 오류까지 모두 clearTokens()/emit()로 처리합니다. 이 경우 refresh token이 아직 유효해도 사용자를 강제로 로그아웃시킬 수 있으니, 토큰 삭제는 서버가 refresh token 무효를 확정한 응답에서만 수행하고 그 외 실패는 토큰을 보존한 채 요청만 실패시키는 편이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt`
around lines 88 - 92, The catch block in HttpClientModule.kt currently treats
any Exception as a guaranteed token-invalidation case and calls
localAuthDataSource.clearTokens() and authEventBus.emit(), which disconnects
users on transient failures; change the handler in the refresh flow (the catch
around the refresh call) to only clear tokens and emit logout when the exception
is a server response that definitively indicates an invalid/expired refresh
token (e.g., inspect ResponseException / HttpResponseException and check
response.status is the specific status the backend uses for invalid refresh
tokens like 401/403 or a dedicated error code), otherwise preserve tokens and
propagate or return a failed refresh result without calling
localAuthDataSource.clearTokens() or authEventBus.emit().
요약
Apple 로그인의 서버 인증 연동과 provider 기반 로그인 분기를 구현하고, Ktor Auth 플러그인을 활용한 토큰 자동 첨부 및 리프레시 기능을 추가했습니다. 통합 테스트를 통해 토큰 만료/갱신 시나리오를 검증합니다.
resolve: #48
변경 사항
Features
feat: Ktor Auth 플러그인 기반 토큰 자동 첨부/리프레시 및 통합 테스트 구현feat: Apple 로그인 서버 인증 연동 및 provider 기반 로그인 분기 구현🤖 Generated with Claude Code
Summary by CodeRabbit
새로운 기능
버그 수정
테스트