Skip to content

feat: add hosted auth - #803

Merged
mikepitre merged 15 commits into
mainfrom
mike/hosted-auth
Jul 27, 2026
Merged

feat: add hosted auth#803
mikepitre merged 15 commits into
mainfrom
mike/hosted-auth

Conversation

@mikepitre

@mikepitre mikepitre commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

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

val result = Clerk.auth.startHostedAuth(
  mode = HostedAuthMode.SIGN_UP,
)

mode defaults 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

  • Creates the hosted-auth transfer through FAPI and relies on the existing response converter to unwrap the standard { response, client } envelope.
  • Opens the Account Portal URL through the SDK's existing Custom Tab activity and callback receiver.
  • Validates the callback, redeems the transfer, and confirms the response contains the session created by Account Portal before activation.
  • Applies the redeemed client to local state as soon as redemption succeeds, before activation, so a failed setActive cannot leave the device signed out against a live server session. This also keeps activation working on instances with forced organization selection.
  • Preserves pending completion across activity recreation and deduplicates repeated callbacks.
  • Keeps hosted-auth request bodies out of logs and delays client synchronization until the callback has been validated.
  • Guards client response side effects so a cancelled or superseded attempt cannot publish stale state. Rotated device tokens are saved unconditionally, since the rotation has already happened server-side by the time the response arrives.
  • Coordinates hosted auth with the existing SSO and external-account browser flows.
  • If an abandoned handoff leaves the native client stale, retries hosted-auth creation once with ClerkGo's replacement device token and omits the old client ID.

Security

  • Uses PKCE with SHA-256 to bind redemption to the app that initiated the flow.
  • Generates cryptographically random state and validates it on callback.
  • Requires the callback scheme, authority, port, and path to match the initiated redirect URL.
  • Requires exactly one state, rotating-token nonce, and created-session ID.
  • Validates callbacks before claiming the pending completion, so a forged callback fired at the exported receiver by another app cannot abort an in-flight flow.
  • Sends the PKCE verifier in a POST body and excludes sensitive hosted-auth payloads from logging.
  • Activates only the session identified by the validated callback.

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

val result = Clerk.auth.startHostedAuth(
  mode = HostedAuthMode.SIGN_UP,
)

mode defaults 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

  • Creates the hosted-auth transfer through FAPI and relies on the existing response converter to unwrap the standard { response, client } envelope.
  • Opens the Account Portal URL through the SDK's existing Custom Tab activity and callback receiver.
  • Validates the callback, redeems the transfer, and confirms the response contains the session created by Account Portal before activation.
  • Applies the redeemed client to local state as soon as redemption succeeds, before activation, so a failed setActive cannot leave the device signed out against a live server session. This also keeps activation working on instances with forced organization selection.
  • Preserves pending completion across activity recreation and deduplicates repeated callbacks.
  • Keeps hosted-auth request bodies out of logs and delays client synchronization until the callback has been validated.
  • Guards client response side effects so a cancelled or superseded attempt cannot publish stale state. Rotated device tokens are saved unconditionally, since the rotation has already happened server-side by the time the response arrives.
  • Coordinates hosted auth with the existing SSO and external-account browser flows.
  • If an abandoned handoff leaves the native client stale, retries hosted-auth creation once with ClerkGo's replacement device token and omits the old client ID.

Security

  • Uses PKCE with SHA-256 to bind redemption to the app that initiated the flow.
  • Generates cryptographically random state and validates it on callback.
  • Requires the callback scheme, authority, port, and path to match the initiated redirect URL.
  • Requires exactly one state, rotating-token nonce, and created-session ID.
  • Validates callbacks before claiming the pending completion, so a forged callback fired at the exported receiver by another app cannot abort an in-flight flow.
  • Sends the PKCE verifier in a POST body and excludes sensitive hosted-auth payloads from logging.
  • Activates only the session identified by the validated callback.

Related PRs

Changes since #803 opened

  • Refactored matchesHostedAuthRedirectUrl function in hosted auth callback handling [7ac6c51]
  • Rewrote hex encoding implementation in SecureRandom.nextHex utility [7ac6c51]
  • Refactored HostedAuthService.createHostedAuth function success-branch handling and added exception propagation documentation [7ac6c51]
  • Changed URI matching in com.clerk.api.hostedauth.Uri.matchesHostedAuthRedirectUrl to perform case-insensitive comparisons for scheme and authority components while keeping path comparison case-sensitive [e43d9dc]
  • Introduced HostedAuthCancellationException exception type for hosted authentication cancellation scenarios [46f3508]
  • Modified SSOManagerActivity to track completion state using Completion enum instead of separate boolean flags [46f3508]
  • Added validation in prepareHostedAuth to reject http and https redirect URL schemes [46f3508]
  • Refactored hosted auth callback validation from dual-method check to single isForgedCallback predicate [46f3508]
  • Flattened PreparedHostedAuth data class structure by removing nested HostedAuthRequest [46f3508]
  • Modified HostedAuthService.redeemAndComplete to consistently route results through completion handlers [46f3508]
  • Removed default parameter value from ClientApi.redeemHostedAuth endpoint [46f3508]
  • Refactored Auth.handle method assignment from if-else expression to boolean short-circuit [46f3508]
  • Modified hosted authentication flow to gate browser/activity launch on pending auth currency at launch time [ad90d37]
  • Added test coverage for cancellation before browser launch scenario [ad90d37]
  • Modified hosted authentication flow to gate browser/activity launch on pending auth currency at launch time [ad90d37]
  • Added test coverage for cancellation before browser launch scenario [ad90d37]
  • Expanded ConfigurationManager pending authentication flow detection to include Hosted Auth flows alongside existing SSO flows [7dd490c]
  • Added test coverage for expanded authentication flow detection in ConfigurationManagerAuthRaceTest [7dd490c]
  • Removed SSO-specific race condition test file ConfigurationManagerSsoRaceTest [7dd490c]

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.
@mikepitre
mikepitre marked this pull request as ready for review July 13, 2026 20:49
@mikepitre
mikepitre requested a review from swolfand July 13, 2026 20:56
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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 Auth, SSO activities, and competing authentication-flow cancellation. Adds request tagging for sensitive logging, guarded client synchronization, safe URI handling, and extensive tests for network, callback, lifecycle, middleware, and activity behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: adding hosted auth support.
Description check ✅ Passed The description is comprehensive and covers summary, API, implementation, security, and related PRs, satisfying the required summary section.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (3)
source/api/src/main/kotlin/com/clerk/api/sso/SSOService.kt (1)

103-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate HostedAuthService.cancelPendingAuthentication(...) call across three entry points.

The same cancellation call is repeated verbatim in authenticateWithRedirect, authenticateSignUpWithRedirect, and authenticateWithPreparedRedirect. Extracting a small helper (e.g. cancelExistingFlows()) alongside the existing currentPendingAuth?.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 win

Rethrow CancellationException before the generic catch.

On the JVM CancellationException is an Exception, so the catch (error: Exception) here swallows cooperative cancellation of the completion job (e.g. when HostedAuthPendingStore.cancel() cancels a running completionJob at Line 287). Today the follow-on finishPendingAuth is 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 win

Add 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9d691f5 and c3f8031.

📒 Files selected for processing (30)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/auth/Auth.kt
  • source/api/src/main/kotlin/com/clerk/api/auth/HostedAuthMode.kt
  • source/api/src/main/kotlin/com/clerk/api/externalaccount/ExternalAccountService.kt
  • source/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthCallback.kt
  • source/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthPkce.kt
  • source/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthService.kt
  • source/api/src/main/kotlin/com/clerk/api/network/ApiPaths.kt
  • source/api/src/main/kotlin/com/clerk/api/network/ClerkApi.kt
  • source/api/src/main/kotlin/com/clerk/api/network/api/ClientApi.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/RequestTags.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/ClientSyncingMiddleware.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/outgoing/RequestLoggingMiddleware.kt
  • source/api/src/main/kotlin/com/clerk/api/network/model/hostedauth/HostedAuthResource.kt
  • source/api/src/main/kotlin/com/clerk/api/sso/SSOManagerActivity.kt
  • source/api/src/main/kotlin/com/clerk/api/sso/SSOReceiverActivity.kt
  • source/api/src/main/kotlin/com/clerk/api/sso/SSOService.kt
  • source/api/src/test/java/com/clerk/api/auth/AuthHandleTest.kt
  • source/api/src/test/java/com/clerk/api/auth/AuthStartHostedAuthTest.kt
  • source/api/src/test/java/com/clerk/api/hostedauth/HostedAuthCallbackTest.kt
  • source/api/src/test/java/com/clerk/api/hostedauth/HostedAuthPkceTest.kt
  • source/api/src/test/java/com/clerk/api/hostedauth/HostedAuthRedirectUrlTest.kt
  • source/api/src/test/java/com/clerk/api/hostedauth/HostedAuthServiceTest.kt
  • source/api/src/test/java/com/clerk/api/network/api/ClientApiTest.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/outgoing/RequestLoggingMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/sso/SSOManagerActivityTest.kt
  • source/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.

@coderabbitai coderabbitai Bot 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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c3f8031 and d88dc37.

📒 Files selected for processing (7)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/ClientSyncingMiddleware.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddlewareTest.kt
  • source/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

@coderabbitai coderabbitai Bot 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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c3f8031 and d88dc37.

📒 Files selected for processing (7)
  • source/api/src/main/kotlin/com/clerk/api/Clerk.kt
  • source/api/src/main/kotlin/com/clerk/api/configuration/ConfigurationManager.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/ClientSyncingMiddleware.kt
  • source/api/src/main/kotlin/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddleware.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.kt
  • source/api/src/test/java/com/clerk/api/network/middleware/incoming/DeviceTokenSavingMiddlewareTest.kt
  • source/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}')
PY

Repository: 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.kt

Repository: clerk/clerk-android

Length of output: 9016


Make the shared-device-token fence counter atomic. @Volatile doesn’t make += 1 atomic, and both reset() and fenceClientResponsesAfterSharedDeviceTokenChange() update this counter. Concurrent calls can drop a fence update and let a stale refresh apply after a token change. Use AtomicInteger.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, then reset() can run before handleSuccessfulRefresh() 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 before updateClerkState().

🤖 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 into DEVICE_TOKEN_UPDATE mode; 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 || true

Repository: 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 || true

Repository: 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 || true

Repository: 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.kt

Repository: 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.

swolfand added 2 commits July 13, 2026 20:54
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.

@coderabbitai coderabbitai Bot 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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 17ad305 and 60e01f1.

📒 Files selected for processing (3)
  • source/api/src/main/kotlin/com/clerk/api/hostedauth/HostedAuthService.kt
  • source/api/src/main/kotlin/com/clerk/api/network/api/ClientApi.kt
  • source/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.
@mikepitre
mikepitre merged commit afc3332 into main Jul 27, 2026
11 checks passed
@mikepitre
mikepitre deleted the mike/hosted-auth branch July 27, 2026 21:21
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