feat: add hosted auth - #803
Conversation
Redeeming a hosted auth transfer consumes the single-use nonce and rotates the client token on the server, so the redemption response is authoritative regardless of how the local flow ends. - Apply the redeemed client via Clerk.updateClient once redemption succeeds, instead of only validating it. Previously local state hinged entirely on the follow-up setActive call: on instances with forced organization selection, setActive consulted stale pre-auth session state and failed even though the server session existed, and any transient setActive failure left the device signed out while the server had a live session. - Save the Authorization response header unconditionally in DeviceTokenSavingMiddleware. The header always carries the server's current token for this client; gating it behind the pending-flow ResponseGuard meant a cancellation racing the redeem response could discard the rotated token and desync the stored credential from the server. The guard still protects client syncing, which is stale-flow state; only the hosted auth requests ever attached a guard, so no other flow relied on it for token saving. - Drop the now-unused ResponseGuard parameter from redeemHostedAuth. Affects the api module.
SSOReceiverActivity is exported, so any app can fire the callback URL with garbage parameters mid-flow. The completion slot was claimed before the callback was validated, which meant a state mismatch completed the pending auth with a failure and discarded the later legitimate callback. Validate the callback before claiming the completion slot: an invalid callback now reports its failure to the caller and leaves the flow waiting, while duplicate deliveries of the legitimate callback still deduplicate into a single redemption. Affects the api module.
…sons Compare only the encoded scheme, authority, and path when matching callback URLs; the decoded comparisons were subsumed by the encoded ones. Extract the repeated "new authentication started" cancellation reason into a shared constant used by SSOService and ExternalAccountService. Affects the api module.
Note that pending hosted auth state lives only in memory (matching the existing SSO machinery), that handle() suspends through validation and redemption for hosted auth callbacks, and that it returns before the session is activated. Also restore the original formatting of an unrelated block in Auth.kt. Affects the api module.
Add tests for validateHostedAuthCallback and authenticationUri edge cases, HostedAuthService double-start, cancel, duplicate-callback, and forged-callback behavior, and the startHostedAuth to setActive wiring including the forced organization selection path. Affects the api module.
780528a to
c3f8031
Compare
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds hosted authentication with sign-in/sign-up modes, PKCE and state generation, callback validation, hosted-auth API endpoints, and single-flow lifecycle management. Integrates hosted-auth callbacks into Estimated code review effort: 4 (Complex) | ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
source/api/src/main/kotlin/com/clerk/api/sso/SSOService.kt (1)
103-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate
HostedAuthService.cancelPendingAuthentication(...)call across three entry points.The same cancellation call is repeated verbatim in
authenticateWithRedirect,authenticateSignUpWithRedirect, andauthenticateWithPreparedRedirect. Extracting a small helper (e.g.cancelExistingFlows()) alongside the existingcurrentPendingAuth?.complete(...)call would reduce the risk that a future redirect-entry method forgets to cancel hosted auth, leaving a stale pending flow.♻️ Proposed refactor
+ private fun cancelCompetingFlows(message: String) { + currentPendingAuth?.complete(ClerkResult.unknownFailure(Exception(message))) + HostedAuthService.cancelPendingAuthentication(HOSTED_AUTH_CANCELLED_BY_NEW_FLOW) + }Then replace each of the three sites with a single call to
cancelCompetingFlows("New authentication started, cancelling previous attempt").Also applies to: 181-181, 240-240
🤖 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 `@source/api/src/main/kotlin/com/clerk/api/sso/SSOService.kt` at line 103, Extract the repeated HostedAuthService.cancelPendingAuthentication(HOSTED_AUTH_CANCELLED_BY_NEW_FLOW) and currentPendingAuth?.complete(...) logic from authenticateWithRedirect, authenticateSignUpWithRedirect, and authenticateWithPreparedRedirect into a shared cancelCompetingFlows helper. Replace each entry-point call with that helper, preserving the existing cancellation behavior and message.source/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthService.kt (1)
137-144: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winRethrow
CancellationExceptionbefore the generic catch.On the JVM
CancellationExceptionis anException, so thecatch (error: Exception)here swallows cooperative cancellation of the completion job (e.g. whenHostedAuthPendingStore.cancel()cancels a runningcompletionJobat Line 287). Today the follow-onfinishPendingAuthis a no-op because the store is no longer current, so the effect is benign, but suppressing cancellation is fragile against future edits.♻️ Proposed fix
completionScope.launch(start = CoroutineStart.LAZY) { try { redeemAndComplete(pendingAuth, callback) + } catch (cancellation: kotlinx.coroutines.CancellationException) { + throw cancellation } catch (error: Exception) { finishPendingAuth(pendingAuth, ClerkResult.unknownFailure(error)) } }🤖 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 `@source/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthService.kt` around lines 137 - 144, Update the completionJob coroutine around redeemAndComplete so CancellationException is caught first and rethrown, allowing cooperative cancellation to propagate. Keep the existing generic Exception handling and finishPendingAuth behavior for non-cancellation failures.source/api/src/test/java/com/clerk/api/network/middleware/outgoing/RequestLoggingMiddlewareTest.kt (1)
14-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a positive-path test for non-sensitive requests.
Only the bypass scenario is verified. Add a companion case asserting that a non-tagged request still produces log output via the wrapped
HttpLoggingInterceptor, so a regression that always skips logging (e.g., inverted condition) would be caught.🤖 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 `@source/api/src/test/java/com/clerk/api/network/middleware/outgoing/RequestLoggingMiddlewareTest.kt` around lines 14 - 47, Add a companion test alongside sensitive request bypasses that builds an otherwise equivalent request without the SensitiveRequest tag, executes it through the wrapped HttpLoggingInterceptor, and asserts the logs collection is non-empty. Keep the existing sensitive-request test unchanged and verify normal requests still produce logging output.
🤖 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.
Nitpick comments:
In `@source/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthService.kt`:
- Around line 137-144: Update the completionJob coroutine around
redeemAndComplete so CancellationException is caught first and rethrown,
allowing cooperative cancellation to propagate. Keep the existing generic
Exception handling and finishPendingAuth behavior for non-cancellation failures.
In `@source/api/src/main/kotlin/com/clerk/api/sso/SSOService.kt`:
- Line 103: Extract the repeated
HostedAuthService.cancelPendingAuthentication(HOSTED_AUTH_CANCELLED_BY_NEW_FLOW)
and currentPendingAuth?.complete(...) logic from authenticateWithRedirect,
authenticateSignUpWithRedirect, and authenticateWithPreparedRedirect into a
shared cancelCompetingFlows helper. Replace each entry-point call with that
helper, preserving the existing cancellation behavior and message.
In
`@source/api/src/test/java/com/clerk/api/network/middleware/outgoing/RequestLoggingMiddlewareTest.kt`:
- Around line 14-47: Add a companion test alongside sensitive request bypasses
that builds an otherwise equivalent request without the SensitiveRequest tag,
executes it through the wrapped HttpLoggingInterceptor, and asserts the logs
collection is non-empty. Keep the existing sensitive-request test unchanged and
verify normal requests still produce logging output.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6a061747-941f-448f-9fb3-71c892111e6d
📒 Files selected for processing (30)
source/api/src/main/kotlin/com/clerk/api/Clerk.ktsource/api/src/main/kotlin/com/clerk/api/auth/Auth.ktsource/api/src/main/kotlin/com/clerk/api/auth/HostedAuthMode.ktsource/api/src/main/kotlin/com/clerk/api/externalaccount/ExternalAccountService.ktsource/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthCallback.ktsource/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthPkce.ktsource/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthService.ktsource/api/src/main/kotlin/com/clerk/api/network/ApiPaths.ktsource/api/src/main/kotlin/com/clerk/api/network/ClerkApi.ktsource/api/src/main/kotlin/com/clerk/api/network/api/ClientApi.ktsource/api/src/main/kotlin/com/clerk/api/network/middleware/RequestTags.ktsource/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/ClientSyncingMiddleware.ktsource/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.ktsource/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/RequestLoggingMiddleware.ktsource/api/src/main/kotlin/com/clerk/api/network/model/hostedauth/HostedAuthResource.ktsource/api/src/main/kotlin/com/clerk/api/sso/SSOManagerActivity.ktsource/api/src/main/kotlin/com/clerk/api/sso/SSOReceiverActivity.ktsource/api/src/main/kotlin/com/clerk/api/sso/SSOService.ktsource/api/src/test/java/com/clerk/api/auth/AuthHandleTest.ktsource/api/src/test/java/com/clerk/api/auth/AuthStartHostedAuthTest.ktsource/api/src/test/java/com/clerk/api/hostedauth/HostedAuthCallbackTest.ktsource/api/src/test/java/com/clerk/api/hostedauth/HostedAuthPkceTest.ktsource/api/src/test/java/com/clerk/api/hostedauth/HostedAuthRedirectUrlTest.ktsource/api/src/test/java/com/clerk/api/hostedauth/HostedAuthServiceTest.ktsource/api/src/test/java/com/clerk/api/network/api/ClientApiTest.ktsource/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.ktsource/api/src/test/java/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddlewareTest.ktsource/api/src/test/java/com/clerk/api/network/middleware/outgoing/RequestLoggingMiddlewareTest.ktsource/api/src/test/java/com/clerk/api/sso/SSOManagerActivityTest.ktsource/api/src/test/java/com/clerk/api/sso/SSOReceiverActivityManifestTest.kt
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
clerk/clerk_go(manual)clerk/dashboard(manual)clerk/accounts(manual)clerk/backoffice(manual)clerk/clerk(manual)clerk/clerk-docs(manual)clerk/cloudflare-workers(manual)
Resolve client-syncing and device-token middleware conflicts while preserving hosted-auth response guards and shared-session freshness checks.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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
`@source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt`:
- Around line 503-512: The fenced branch in the refresh flow must not return
immediate success after detecting a device-token generation mismatch. Update the
logic around refreshClientAndEnvironment to queue or await the replacement
outside the current mutex, preserve the original refresh mode and result, and
propagate replacement failures so initialization retry/error handling and
updateDeviceToken() completion reflect the actual refresh outcome.
- Around line 271-273: Update the refresh completion flow around
handleSuccessfulRefresh() and updateClerkState() so reset() cannot invalidate
the fence between the check and final state application. Synchronize the final
state update with the same lock used for the fence/state reset, or perform a
second fence check immediately before updateClerkState(), and skip applying
stale client/environment state when the fence has changed.
- Around line 130-132: The shared-device-token fence generation updates are not
atomic, so concurrent reset and fencing calls can lose increments. Replace
sharedDeviceTokenFenceGeneration with an AtomicInteger, update both reset() and
fenceClientResponsesAfterSharedDeviceTokenChange() to use atomic increment
operations and reads, and add a concurrent regression test covering overlapping
updates and stale-response rejection.
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt`:
- Around line 15-19: Make the read/compare/save sequence in
DeviceTokenSavingMiddleware atomic by protecting it with a lock or mutex shared
by all concurrent middleware instances. Acquire the guard before reading and
comparing the stored token, keep the staleness check and save within the
critical section, and release it reliably after completion so concurrent
responses cannot overwrite a newer token.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 616ba27a-890a-4f5e-a314-e0f6ee803d64
📒 Files selected for processing (7)
source/api/src/main/kotlin/com/clerk/api/Clerk.ktsource/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.ktsource/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/ClientSyncingMiddleware.ktsource/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.ktsource/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.ktsource/api/src/test/java/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddlewareTest.ktsource/api/src/test/java/com/clerk/api/sharedsession/SharedSessionSyncPublicApiTest.kt
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
clerk/clerk_go(manual)clerk/dashboard(manual)clerk/accounts(manual)clerk/backoffice(manual)clerk/clerk(manual)clerk/clerk-docs(manual)clerk/cloudflare-workers(manual)
🚧 Files skipped from review as they are similar to previous changes (2)
- source/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.kt
- source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/ClientSyncingMiddleware.kt
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 4
🤖 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
`@source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt`:
- Around line 503-512: The fenced branch in the refresh flow must not return
immediate success after detecting a device-token generation mismatch. Update the
logic around refreshClientAndEnvironment to queue or await the replacement
outside the current mutex, preserve the original refresh mode and result, and
propagate replacement failures so initialization retry/error handling and
updateDeviceToken() completion reflect the actual refresh outcome.
- Around line 271-273: Update the refresh completion flow around
handleSuccessfulRefresh() and updateClerkState() so reset() cannot invalidate
the fence between the check and final state application. Synchronize the final
state update with the same lock used for the fence/state reset, or perform a
second fence check immediately before updateClerkState(), and skip applying
stale client/environment state when the fence has changed.
- Around line 130-132: The shared-device-token fence generation updates are not
atomic, so concurrent reset and fencing calls can lose increments. Replace
sharedDeviceTokenFenceGeneration with an AtomicInteger, update both reset() and
fenceClientResponsesAfterSharedDeviceTokenChange() to use atomic increment
operations and reads, and add a concurrent regression test covering overlapping
updates and stale-response rejection.
In
`@source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt`:
- Around line 15-19: Make the read/compare/save sequence in
DeviceTokenSavingMiddleware atomic by protecting it with a lock or mutex shared
by all concurrent middleware instances. Acquire the guard before reading and
comparing the stored token, keep the staleness check and save within the
critical section, and release it reliably after completion so concurrent
responses cannot overwrite a newer token.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 616ba27a-890a-4f5e-a314-e0f6ee803d64
📒 Files selected for processing (7)
source/api/src/main/kotlin/com/clerk/api/Clerk.ktsource/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.ktsource/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/ClientSyncingMiddleware.ktsource/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.ktsource/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.ktsource/api/src/test/java/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddlewareTest.ktsource/api/src/test/java/com/clerk/api/sharedsession/SharedSessionSyncPublicApiTest.kt
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
clerk/clerk_go(manual)clerk/dashboard(manual)clerk/accounts(manual)clerk/backoffice(manual)clerk/clerk(manual)clerk/clerk-docs(manual)clerk/cloudflare-workers(manual)
🚧 Files skipped from review as they are similar to previous changes (2)
- source/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.kt
- source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/ClientSyncingMiddleware.kt
🛑 Comments failed to post (4)
source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt (3)
130-132: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail echo '--- outline ---' ast-grep outline source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt --view expanded || true echo '--- relevant lines ---' nl -ba source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt | sed -n '110,330p' echo '--- fence usages ---' rg -n "sharedDeviceTokenFenceGeneration|fenceClientResponsesAfterSharedDeviceTokenChange|reset\\(|sharedDeviceToken" source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt source/api/src/main/kotlin/com/clerk/api -g '*.kt'Repository: clerk/clerk-android
Length of output: 3292
🏁 Script executed:
#!/bin/bash set -euo pipefail echo '--- file around the fence ---' nl -ba source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt | sed -n '120,320p' echo '--- search for concurrency primitives ---' rg -n "synchronized|Mutex|AtomicInteger|volatile|withContext|async|Deferred|await" source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt source/api/src/main/kotlin/com/clerk/api -g '*.kt'Repository: clerk/clerk-android
Length of output: 227
🏁 Script executed:
#!/bin/bash set -euo pipefail python3 - <<'PY' from pathlib import Path p = Path('source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt') text = p.read_text() for needle in [ 'sharedDeviceTokenFenceGeneration', 'fenceClientResponsesAfterSharedDeviceTokenChange', 'reset(', 'loadSharedSession', 'refresh', ]: print(f'### {needle}') for i, line in enumerate(text.splitlines(), 1): if needle in line: print(f'{i}: {line}') PYRepository: clerk/clerk-android
Length of output: 3307
🏁 Script executed:
#!/bin/bash set -euo pipefail nl -ba source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt | sed -n '1,380p'Repository: clerk/clerk-android
Length of output: 197
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "inspection needed"Repository: clerk/clerk-android
Length of output: 175
🏁 Script executed:
#!/bin/bash set -euo pipefail sed -n '260,520p' source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.ktRepository: clerk/clerk-android
Length of output: 9016
Make the shared-device-token fence counter atomic.
@Volatiledoesn’t make+= 1atomic, and bothreset()andfenceClientResponsesAfterSharedDeviceTokenChange()update this counter. Concurrent calls can drop a fence update and let a stale refresh apply after a token change. UseAtomicInteger.incrementAndGet()or guard all access with the same lock, and add a concurrent regression test.🤖 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 `@source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt` around lines 130 - 132, The shared-device-token fence generation updates are not atomic, so concurrent reset and fencing calls can lose increments. Replace sharedDeviceTokenFenceGeneration with an AtomicInteger, update both reset() and fenceClientResponsesAfterSharedDeviceTokenChange() to use atomic increment operations and reads, and add a concurrent regression test covering overlapping updates and stale-response rejection.
271-273: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail FILE="source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt" echo "== Outline ==" ast-grep outline "$FILE" --view expanded || true echo echo "== Relevant symbols ==" rg -n "sharedDeviceTokenFenceGeneration|executeRefresh|handleSuccessfulRefresh|reset\\(" "$FILE" echo echo "== Context around reset and refresh logic ==" sed -n '230,360p' "$FILE" echo sed -n '440,540p' "$FILE"Repository: clerk/clerk-android
Length of output: 11407
🏁 Script executed:
#!/bin/bash set -euo pipefail FILE="source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt" sed -n '552,820p' "$FILE"Repository: clerk/clerk-android
Length of output: 8119
Synchronize the fence check with the state update.
executeRefresh()can pass the device-token fence check, thenreset()can run beforehandleSuccessfulRefresh()writes client/environment state. Cancellation is cooperative, so this can still repopulate SDK state after a reset. Guard the final state application with the same lock or a second fence check immediately beforeupdateClerkState().🤖 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 `@source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt` around lines 271 - 273, Update the refresh completion flow around handleSuccessfulRefresh() and updateClerkState() so reset() cannot invalidate the fence between the check and final state application. Synchronize the final state update with the same lock used for the fence/state reset, or perform a second fence check immediately before updateClerkState(), and skip applying stale client/environment state when the fence has changed.Source: MCP tools
503-512: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Propagate fenced refresh results instead of returning success.
This branch launches a replacement refresh and immediately returns
ClerkResult.success(Unit). During initialization, replacement failures skip initialization error/retry handling because the replacement is forced intoDEVICE_TOKEN_UPDATEmode;updateDeviceToken()can also report success before the replacement finishes. Queue or await the replacement outside the current mutex and preserve the original mode/result semantics.🤖 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 `@source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt` around lines 503 - 512, The fenced branch in the refresh flow must not return immediate success after detecting a device-token generation mismatch. Update the logic around refreshClientAndEnvironment to queue or await the replacement outside the current mutex, preserve the original refresh mode and result, and propagate replacement failures so initialization retry/error handling and updateDeviceToken() completion reflect the actual refresh outcome.source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt (1)
15-19: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail echo '--- file outline ---' ast-grep outline source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt --view expanded || true echo '--- file contents ---' cat -n source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt echo '--- related tests/search ---' rg -n "DeviceTokenSavingMiddleware|DEVICE_TOKEN|AUTHORIZATION_HEADER|shared-session|ResponseGuard" source/api/src/test source/api/src/main -S || trueRepository: clerk/clerk-android
Length of output: 17928
🏁 Script executed:
#!/bin/bash set -euo pipefail ast-grep outline source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt --view expanded || true echo '---' cat -n source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt | sed -n '1,120p' echo '--- tests/search ---' rg -n "DeviceTokenSavingMiddleware|DEVICE_TOKEN|AUTHORIZATION_HEADER|shared-session|ResponseGuard" source/api/src/test source/api/src/main -S || trueRepository: clerk/clerk-android
Length of output: 17881
🏁 Script executed:
#!/bin/bash set -euo pipefail # Search for device-token semantics and middleware usage. rg -n "device token|DEVICE_TOKEN|shared-session|AUTHORIZATION_HEADER|DeviceTokenSavingMiddleware|ResponseGuard" \ source/api src test . -S || true # Show nearby declarations if found. fd -a "StorageHelper.kt|StorageKey.kt|DeviceTokenSavingMiddleware.kt|ResponseGuard.kt" source/api src || trueRepository: clerk/clerk-android
Length of output: 39618
🏁 Script executed:
#!/bin/bash set -euo pipefail echo '--- StorageHelper excerpt ---' sed -n '1,220p' source/api/src/main/kotlin/com/clerk/api/storage/StorageHelper.kt echo '--- DeviceTokenSavingMiddleware test excerpt ---' sed -n '1,220p' source/api/src/test/java/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddlewareTest.kt echo '--- SharedSessionSyncCoordinator excerpt around token handling ---' sed -n '150,210p' source/api/src/main/kotlin/com/clerk/api/sharedsession/SharedSessionSyncCoordinator.ktRepository: clerk/clerk-android
Length of output: 11647
🏁 Script executed:
#!/bin/bash set -euo pipefail # Show the StorageHelper methods that read/write DEVICE_TOKEN and any synchronization around them. nl -ba source/api/src/main/kotlin/com/clerk/api/storage/StorageHelper.kt | sed -n '1,220p'Repository: clerk/clerk-android
Length of output: 197
Make the device-token update atomic The read/compare/save block at lines 32-36 is still racy: two concurrent responses can both pass the staleness check, and the later write can clobber a newer token. Guard it with a shared lock or mutex.
🤖 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 `@source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt` around lines 15 - 19, Make the read/compare/save sequence in DeviceTokenSavingMiddleware atomic by protecting it with a lock or mutex shared by all concurrent middleware instances. Acquire the guard before reading and comparing the stored token, keep the staleness check and save within the critical section, and release it reliably after completion so concurrent responses cannot overwrite a newer token.
Reject stale redemption results and invalid receiver callbacks. Preserve coroutine cancellation and consolidate competing-flow cleanup while expanding middleware logging coverage.
Normalize the shared-session merge hunks with the Gradle Spotless formatter so CI and the committed tree agree.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@source/api/src/test/java/com/clerk/api/hostedauth/HostedAuthServiceTest.kt`:
- Around line 103-140: Extend HostedAuthCreateRequest capture in
startRetriesCreateOnceAfterUnauthorizedWithSameRequest to record the
SensitiveRequest tag passed by createHostedAuth, then assert the first and retry
requests contain the same tag. Preserve the existing assertions for all other
request fields.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: dfab5283-c58c-44de-9462-977d96332d42
📒 Files selected for processing (3)
source/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthService.ktsource/api/src/main/kotlin/com/clerk/api/network/api/ClientApi.ktsource/api/src/test/java/com/clerk/api/hostedauth/HostedAuthServiceTest.kt
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
clerk/clerk_go(manual)clerk/dashboard(manual)clerk/accounts(manual)clerk/backoffice(manual)clerk/clerk(manual)clerk/clerk-docs(manual)clerk/cloudflare-workers(manual)
🚧 Files skipped from review as they are similar to previous changes (2)
- source/api/src/main/kotlin/com/clerk/api/network/api/ClientApi.kt
- source/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthService.kt
…on state - Add public HostedAuthCancellationException so cancellation is branchable on ClerkResult.Failure.throwable without matching message strings. - Reject http(s) redirect URLs before creating hosted auth, matching the Expo and iOS SDKs' fail-fast behavior. - Replace SSOManagerActivity's completion booleans with a persisted Completion enum and document the re-attach invariant. - Flatten prepareHostedAuth, drop the redundant isCurrent wrapper in redeemAndComplete, replace isValidCallback with isForgedCallback, and make the redeem call's ManualClientSyncRequest explicit.
A cancellation from a competing flow could land between the ownership check and startActivity, launching the hosted auth browser on top of the newly started flow. Since SSOManagerActivity is singleTask, the stray intent could clobber that flow's authorization. Running the launch inside runIfCurrent makes launch-vs-cancel atomic.
Returning from the Chrome Custom Tab foregrounds the app, which triggers ConfigurationManager's lifecycle callback. That callback defers its refresh while a browser-based auth flow is still in flight, but the guard only consulted SSOService. Hosted auth was omitted, so the refresh ran unguarded, rotated the device token, and the in-flight redemption then failed its response-freshness check in HostedAuthService.redeemAndComplete. Users saw the sign-in fail with "Hosted auth redemption response is no longer current." It is a race, so it only reproduced when enough time passed in the browser for the foreground refresh to win — reliably reproducible by waiting ~45s on the Account Portal OTP screen before submitting the code. Add HostedAuthService.hasPendingAuthentication() to the guard, and rename hasPendingSsoFlow/deferForegroundRefreshDuringPendingSso to hasPendingAuthFlow/deferForegroundRefreshDuringPendingAuth since they now cover more than SSO. Both are internal/private, so the rename is contained.
Summary
Adds hosted auth to Clerk Android. An app can open Account Portal in a Custom Tab for sign-in or sign-up, then activate the resulting session on the native Clerk client.
Account Portal continues to own the authentication flow. The Android SDK handles the final native handoff: it validates the callback, redeems the one-time transfer into the current client, and activates the session Account Portal created.
API
modedefaults to sign-in. The SDK uses its registered callback URL by default, with an override available for apps that register and forward a custom callback.Implementation
{ response, client }envelope.setActivecannot leave the device signed out against a live server session. This also keeps activation working on instances with forced organization selection.Security
Related PRs
Note
Summary
Adds hosted auth to Clerk Android. An app can open Account Portal in a Custom Tab for sign-in or sign-up, then activate the resulting session on the native Clerk client.
Account Portal continues to own the authentication flow. The Android SDK handles the final native handoff: it validates the callback, redeems the one-time transfer into the current client, and activates the session Account Portal created.
API
modedefaults to sign-in. The SDK uses its registered callback URL by default, with an override available for apps that register and forward a custom callback.Implementation
{ response, client }envelope.setActivecannot leave the device signed out against a live server session. This also keeps activation working on instances with forced organization selection.Security
Related PRs
Changes since #803 opened
matchesHostedAuthRedirectUrlfunction in hosted auth callback handling [7ac6c51]SecureRandom.nextHexutility [7ac6c51]HostedAuthService.createHostedAuthfunction success-branch handling and added exception propagation documentation [7ac6c51]com.clerk.api.hostedauth.Uri.matchesHostedAuthRedirectUrlto perform case-insensitive comparisons for scheme and authority components while keeping path comparison case-sensitive [e43d9dc]HostedAuthCancellationExceptionexception type for hosted authentication cancellation scenarios [46f3508]SSOManagerActivityto track completion state usingCompletionenum instead of separate boolean flags [46f3508]prepareHostedAuthto reject http and https redirect URL schemes [46f3508]isForgedCallbackpredicate [46f3508]PreparedHostedAuthdata class structure by removing nestedHostedAuthRequest[46f3508]HostedAuthService.redeemAndCompleteto consistently route results through completion handlers [46f3508]ClientApi.redeemHostedAuthendpoint [46f3508]Auth.handlemethod assignment from if-else expression to boolean short-circuit [46f3508]ConfigurationManagerpending authentication flow detection to include Hosted Auth flows alongside existing SSO flows [7dd490c]ConfigurationManagerAuthRaceTest[7dd490c]ConfigurationManagerSsoRaceTest[7dd490c]