Skip to content

Align Android session token fetching with edge minter behavior - #827

Draft
swolfand wants to merge 1 commit into
mainfrom
sam/mobile-598-align-android-to-the-new-js-behavior
Draft

Align Android session token fetching with edge minter behavior#827
swolfand wants to merge 1 commit into
mainfrom
sam/mobile-598-align-android-to-the-new-js-behavior

Conversation

@swolfand

@swolfand swolfand commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • decode the session_minter environment flag and send organization, previous-token, and force-origin parameters for default token requests
  • scope token caches by organization or template and hydrate them from the current session snapshot
  • keep the freshest token when forced refreshes complete out of order while preserving ordinary request deduplication
  • reset shared in-flight token state when the Clerk runtime is reset or reconfigured

Why

Android did not yet implement the session-minter request and freshness behavior used by clerk-js and the corresponding iOS implementation. A late stale response could overwrite a newer canonical session token, and forced refreshes were deduplicated even though each request must reach the token endpoint.

This aligns Android with the new behavior tracked in MOBILE-598 and prevents concurrent token refreshes from rolling back the cache.

Validation

  • ./gradlew :source:api:testDebugUnitTest
  • ./gradlew :source:api:spotlessCheck
  • ./gradlew :source:api:detekt
  • ./gradlew :source:api:lintDebug
  • git diff --check

Related iOS implementation: clerk/clerk-ios#534

Note

Align Android session token fetching with edge minter behavior

  • Adds a sessionMinter flag to AuthConfig decoded from the environment response, which gates new minter-specific behavior in token fetching.
  • Rewrites SessionTokenFetcher to use a singleton (shared), deduplicate in-flight requests by cache key (template or active organization), and skip deduplication when skipCache is true.
  • For non-template fetches, posts organization_id to SessionApi.tokens; when sessionMinter is enabled, also sends the previous token and sets force_origin=true on forced refreshes.
  • Introduces TokenFreshness to compare JWT metadata (oiat, iat, org/session ID) so out-of-order responses never overwrite a fresher cached token.
  • Adds SessionTokensCache.storeIfFresher and hydrate for atomic, freshness-gated cache writes.
  • reset() on SessionTokenFetcher.shared is called during client/session clear and configuration reset to cancel in-flight tasks.

🖇️ Linked Issues

Resolves MOBILE-598, which tracks aligning Android session token fetching to the equivalent changes made in clerk-js and clerk-ios.

📊 Macroscope summarized 6342742. 8 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 100b05b0-8581-4573-a95d-9de342eb8456

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

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

Comment on lines +45 to +47
return if (jwt == null || tokenSessionId == null) {
true
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium session/TokenFreshness.kt:45

When the JWT cannot be decoded or has no sid claim, matches returns true, so the token is treated as belonging to any session/organization. fetchToken then hydrates that session's cache with this token, causing a token lacking session identity claims to be cached and served under an unrelated session/organization instead of forcing a fresh fetch. Missing identity/context claims should fail the match rather than return true.

Suggested change
return if (jwt == null || tokenSessionId == null) {
true
} else {
return if (jwt == null || tokenSessionId == null) {
false
} else {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @source/api/src/main/kotlin/com/clerk/api/session/TokenFreshness.kt around lines 45-47:

When the JWT cannot be decoded or has no `sid` claim, `matches` returns `true`, so the token is treated as belonging to any session/organization. `fetchToken` then hydrates that session's cache with this token, causing a token lacking session identity claims to be cached and served under an unrelated session/organization instead of forcing a fresh fetch. Missing identity/context claims should fail the match rather than return `true`.

Comment on lines +195 to 196
SessionTokensCache.storeIfFresher(cacheKey, tokensRequest.value)
tokensRequest.value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High session/SessionTokenFetcher.kt:195

fetchToken stores the response via SessionTokensCache.storeIfFresher(...) but then returns tokensRequest.value instead of the canonicalToken that call returns. When two skipCache requests complete out of order, the late, stale response is correctly prevented from replacing the cache, but that stale response's caller still receives the stale JWT rather than the fresher canonical token — defeating the freshness guarantee for the immediate result. Consider returning the StoreResult.canonicalToken instead of the raw response.

Suggested change
SessionTokensCache.storeIfFresher(cacheKey, tokensRequest.value)
tokensRequest.value
SessionTokensCache.storeIfFresher(cacheKey, tokensRequest.value).canonicalToken
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @source/api/src/main/kotlin/com/clerk/api/session/SessionTokenFetcher.kt around lines 195-196:

`fetchToken` stores the response via `SessionTokensCache.storeIfFresher(...)` but then returns `tokensRequest.value` instead of the `canonicalToken` that call returns. When two `skipCache` requests complete out of order, the late, stale response is correctly prevented from replacing the cache, but that stale response's caller still receives the stale JWT rather than the fresher canonical token — defeating the freshness guarantee for the immediate result. Consider returning the `StoreResult.canonicalToken` instead of the raw response.

/**
* Releases deduplicated waiters and removes requests registered by the previous Clerk runtime.
*/
internal fun reset() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High session/SessionTokenFetcher.kt:57

reset() cancels the waiter Deferreds but not the in-flight fetchToken coroutine, so an ongoing token request can keep running after reset, write its JWT into the shared SessionTokensCache, and return a token from the old configuration to the caller. This stale credential can contaminate the new runtime. Consider cancelling or fencing the actual fetch work so an in-flight request cannot store results after reset.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @source/api/src/main/kotlin/com/clerk/api/session/SessionTokenFetcher.kt around line 57:

`reset()` cancels the waiter `Deferred`s but not the in-flight `fetchToken` coroutine, so an ongoing token request can keep running after reset, write its JWT into the shared `SessionTokensCache`, and return a token from the old configuration to the caller. This stale credential can contaminate the new runtime. Consider cancelling or fencing the actual fetch work so an in-flight request cannot store results after reset.

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.

1 participant