Adds a mobile token provider to the SDK - #123
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
WalkthroughChangesMobile-token authentication now supports provider-based configuration, dynamic authorization, token refresh, and one retry after a 401 response. The example app demonstrates token-provider configuration and result logging. Mobile-token authentication
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Chargebee
participant TokenProvider
participant Retrofit
participant MobileTokenAuthenticator
Chargebee->>TokenProvider: Request initial or refreshed token
TokenProvider-->>Chargebee: Return token
Chargebee->>Retrofit: Send request with mobile-token authorization
Retrofit-->>MobileTokenAuthenticator: Return 401 response
MobileTokenAuthenticator->>Chargebee: Request token refresh
Chargebee->>TokenProvider: Invoke provider
TokenProvider-->>Chargebee: Return refreshed token
MobileTokenAuthenticator->>Retrofit: Retry request with refreshed authorization
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@chargebee/src/main/java/com/chargebee/android/Chargebee.kt`:
- Around line 159-167: Scope token refresh and authentication callbacks to the
active configuration so callbacks from an older configure call cannot update
mobileToken or authenticate requests for the new site. Add a configuration
generation/version, capture it when starting each refresh, and ignore callbacks
whose generation no longer matches; also clear mobileToken before the
token-provider overload refreshes. Add a delayed-provider regression test
covering reconfiguration and stale callback delivery.
In
`@chargebee/src/main/java/com/chargebee/android/network/MobileTokenAuthenticator.kt`:
- Around line 22-29: Update the authenticator logic around refreshTokenBlocking
so retry tracking uses a request-scoped marker rather than
response.priorResponse(), allowing token refresh after a redirect-then-401 while
preventing a second authenticator retry. Mark the rebuilt request before
returning it, preserve null when that marker is already present, and add
coverage verifying the provider runs once followed by exactly one retry.
🪄 Autofix
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: Enterprise
Run ID: 671cb88d-78eb-487c-83cc-511951c38795
📒 Files selected for processing (3)
chargebee/src/main/java/com/chargebee/android/Chargebee.ktchargebee/src/main/java/com/chargebee/android/network/MobileTokenAuthenticator.ktchargebee/src/main/java/com/chargebee/android/resources/BaseResource.kt
| this.site = site | ||
| this.publishableApiKey = "" | ||
| this.encodedPublishableApiKey = "" | ||
| this.baseUrl = "https://${site}.chargebee.com/api/" | ||
| this.allowErrorLogging = allowErrorLogging | ||
| this.sdkKey = sdkKey | ||
| this.applicationId = packageName | ||
| this.tokenProvider = tokenProvider | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Chargebee.kt relevant declarations and methods ---'
rg -n -C 8 'mobileToken|encodedApiKey|configure|refreshMobileToken|tokenProvider|publishableApiKey|baseUrl' chargebee/src/main/java/com/chargebee/android/Chargebee.kt
printf '%s\n' '--- related usages ---'
rg -n -C 3 'encodedApiKey|mobileToken|refreshMobileToken|CBMobileTokenProvider' chargebee/src test* 2>/dev/null || true
printf '%s\n' '--- file outline ---'
ast-grep outline chargebee/src/main/java/com/chargebee/android/Chargebee.kt 2>/dev/null || trueRepository: chargebee/chargebee-android
Length of output: 31015
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- MobileTokenAuthenticator ---'
cat -n chargebee/src/main/java/com/chargebee/android/network/MobileTokenAuthenticator.kt
printf '%s\n' '--- bounded interleaving verifier ---'
python3 - <<'PY'
import base64
state = {
"site": "",
"mobileToken": "",
"tokenProvider": None,
"encodedPublishableApiKey": "",
}
def configure_token(site, provider):
state["site"] = site
state["encodedPublishableApiKey"] = ""
state["tokenProvider"] = provider
# The implementation calls refreshMobileToken, which captures provider.
return provider
def callback(provider, token):
# Exact behavior of Chargebee.refreshMobileToken's callback.
if token:
state["mobileToken"] = token
return True
return False
def encoded_api_key():
token = state["mobileToken"]
if token:
raw = f"{token}:".encode()
return "Basic " + base64.b64encode(raw).decode()
return state["encodedPublishableApiKey"]
provider_a = lambda: None
provider_b = lambda: None
captured_a = configure_token("site-a", provider_a)
captured_b = configure_token("site-b", provider_b)
assert state["site"] == "site-b"
assert captured_a is provider_a and captured_b is provider_b
callback(captured_a, "token-a")
assert state["mobileToken"] == "token-a"
print({
"current_site": state["site"],
"token_after_old_callback": state["mobileToken"],
"authorization_header": encoded_api_key(),
"stale_token_selected": state["site"] == "site-b" and state["mobileToken"] == "token-a",
})
PYRepository: chargebee/chargebee-android
Length of output: 2567
Sensitive Data Exposure (CWE-362): Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')
Reachability: Internal · Exploitability: Moderate
Scope token refresh results to their originating configuration.
An older provider callback can overwrite mobileToken after a new configure call. Requests can then send the previous tenant's token to the new site's endpoint. The token-provider overload also does not clear the existing token before refresh. Reject stale refresh and authentication callbacks with a configuration generation, and add a delayed-provider 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 `@chargebee/src/main/java/com/chargebee/android/Chargebee.kt` around lines 159
- 167, Scope token refresh and authentication callbacks to the active
configuration so callbacks from an older configure call cannot update
mobileToken or authenticate requests for the new site. Add a configuration
generation/version, capture it when starting each refresh, and ignore callbacks
whose generation no longer matches; also clear mobileToken before the
token-provider overload refreshes. Add a delayed-provider regression test
covering reconfiguration and stale callback delivery.
Source: Path instructions
| // Retry only once: a non-null priorResponse means we already refreshed and retried. | ||
| if (response.priorResponse() != null) { | ||
| return null | ||
| } | ||
| val refreshedHeader = refreshTokenBlocking() ?: return null | ||
| return response.request().newBuilder() | ||
| .header("Authorization", refreshedHeader) | ||
| .build() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate file ---'
fd -i 'MobileTokenAuthenticator.kt' . -x sh -c 'cat -n "$1"' sh {}
printf '%s\n' '--- OkHttp declarations and authenticator usage ---'
rg -n -S 'okhttp|Authenticator|MobileTokenAuthenticator|priorResponse|refreshTokenBlocking' \
-g '!build' -g '!**/node_modules/**' .
printf '%s\n' '--- relevant dependency files ---'
fd -i '(build.gradle|build.gradle.kts|libs.versions.toml|gradle.properties|pom.xml)' . -x sh -c '
if rg -n -i "okhttp|com.squareup.okhttp" "$1" >/dev/null; then
echo "### $1"
rg -n -i -C 2 "okhttp|com.squareup.okhttp" "$1"
fi
' sh {}Repository: chargebee/chargebee-android
Length of output: 4446
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- project files related to dependencies ---'
git ls-files | rg '(^|/)(build\.gradle(\.kts)?|settings\.gradle(\.kts)?|libs\.versions\.toml|gradle\.properties|pom\.xml)$|(^|/)gradle/'
printf '%s\n' '--- dependency declarations ---'
rg -n -i -C 3 'okhttp|com\.squareup\.okhttp|com\.squareup\.okhttp3' \
$(git ls-files | rg '(^|/)(build\.gradle(\.kts)?|settings\.gradle(\.kts)?|libs\.versions\.toml|gradle\.properties|pom\.xml)$') || true
printf '%s\n' '--- client construction ---'
cat -n chargebee/src/main/java/com/chargebee/android/resources/BaseResource.ktRepository: chargebee/chargebee-android
Length of output: 1613
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- root build configuration ---'
cat -n build.gradle
printf '%s\n' '--- chargebee module build configuration ---'
cat -n chargebee/build.gradle
printf '%s\n' '--- settings and properties ---'
cat -n settings.gradle
cat -n gradle.properties
printf '%s\n' '--- tests and mocks related to authentication or redirects ---'
rg -n -i -C 4 'MobileTokenAuthenticator|authenticator|401|redirect|tokenProvider|refreshMobileToken' \
chargebee app -g '*Test*' -g '*test*' -g '*.kt' -g '*.java' || trueRepository: chargebee/chargebee-android
Length of output: 21784
🌐 Web query:
OkHttp Response.priorResponse redirects authenticator 401 documentation RetryAndFollowUpInterceptor
💡 Result:
In OkHttp, the Response.priorResponse property and the Authenticator interface are key components for handling authentication challenges (such as 401 Unauthorized or 407 Proxy Authentication Required) within the RetryAndFollowUpInterceptor [1][2][3]. 1. Response.priorResponse The priorResponse property returns the previous response that triggered the current request (e.g., a 401 challenge) [1][4]. If the current request was not triggered by an automatic retry, this returns null [1]. This property is primarily used to detect previous attempts to authenticate or follow redirects, helping prevent infinite request-response loops [2][5]. 2. Authenticator and 401 Handling When a server returns a 401 (Unauthorized) status code, OkHttp calls the registered Authenticator implementation [2][6]. The authenticator is responsible for providing a new request with the appropriate "Authorization" header or returning null to decline the challenge [2][7]. To prevent infinite loops when credentials are invalid or expired, the standard practice is to check the current request's headers before attempting to re-authenticate [2][7][8]. If the request already contains an "Authorization" header, the authenticator should return null [2][8]. 3. RetryAndFollowUpInterceptor This internal interceptor is responsible for coordinating redirects, retries, and authentication challenges [9][3][6]. When it receives a 401 response, it calls the Authenticator [3][6]. If the authenticator returns a new, non-null request, the interceptor retries the call with this new request [3][6]. 4. Detecting Attempt Counts If you need to limit the number of authentication retries, you can calculate the current attempt count by traversing the chain of prior responses [2][5]. The following common pattern is used for this purpose: private fun responseCount(response: Response): Int { var result = 1 var prior = response.priorResponse while (prior!= null) { result++ prior = prior.priorResponse } return result } By checking this count or simply verifying if an "Authorization" header is already present, you can safely abort authentication attempts if they are not successful [2][7][8].
Citations:
- 1: https://square.github.io/okhttp/5.x/okhttp/okhttp3/-response/prior-response.html
- 2: https://square.github.io/okhttp/4.x/okhttp/okhttp3/-authenticator/index.html
- 3: https://github.com/square/okhttp/blob/07309c1c7d9e296014268ebd155ebf7ef8679f6c/okhttp/src/main/java/okhttp3/internal/http/RetryAndFollowUpInterceptor.java
- 4: https://square.github.io/okhttp/5.x/okhttp/okhttp3/-response/index.html
- 5: Infinite request/response loop when using an Authenticator with invalid credentials lysine-dev/okhttp#960
- 6: https://github.com/square/okhttp/blob/master/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt
- 7: https://square.github.io/okhttp/3.x/okhttp/okhttp3/Authenticator.html
- 8: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/Authenticate.kt
- 9: https://github.com/square/okhttp/blob/okhttp_3.12.x/okhttp/src/main/java/okhttp3/internal/http/RetryAndFollowUpInterceptor.java
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Retrofit 2.9.0 resolved OkHttp dependency ---'
curl -fsSL https://repo1.maven.org/maven2/com/squareup/retrofit2/retrofit/2.9.0/retrofit-2.9.0.pom |
sed -n '/<artifactId>okhttp<\/artifactId>/,/<\/dependency>/p'
printf '%s\n' '--- OkHttp 3.14.9 redirect and authentication response-chain logic ---'
curl -fsSL https://raw.githubusercontent.com/square/okhttp/okhttp_3.14.x/okhttp/src/main/java/okhttp3/internal/http/RetryAndFollowUpInterceptor.java |
rg -n -C 5 'priorResponse|followUpRequest|followRedirects|401|Authenticator'
printf '%s\n' '--- behavioral probe for the guard ---'
python3 - <<'PY'
class Response:
def __init__(self, code, prior=None):
self.code = code
self.prior = prior
def authenticate(response, provider_configured=True):
if not provider_configured:
return None
if response.prior is not None:
return None
return "retry-with-refreshed-header"
cases = {
"direct 401": Response(401),
"redirect then 401": Response(401, Response(302)),
"redirect chain then 401": Response(401, Response(302, Response(301))),
}
for name, response in cases.items():
print(f"{name}: {authenticate(response)!r}")
PYRepository: chargebee/chargebee-android
Length of output: 4553
Track this authenticator’s retry separately from priorResponse().
When OkHttp follows a redirect before returning 401, priorResponse() is non-null. authenticate() then returns null and skips token refresh. Use a request-scoped retry marker and add a redirect-then-401 test. The provider must run once, followed by one retry.
🤖 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
`@chargebee/src/main/java/com/chargebee/android/network/MobileTokenAuthenticator.kt`
around lines 22 - 29, Update the authenticator logic around refreshTokenBlocking
so retry tracking uses a request-scoped marker rather than
response.priorResponse(), allowing token refresh after a redirect-then-401 while
preventing a second authenticator retry. Mark the rebuilt request before
returning it, preserve null when that marker is already present, and add
coverage verifying the provider runs once followed by exactly one retry.
Source: Path instructions
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 `@app/src/main/java/com/chargebee/example/MainActivity.kt`:
- Around line 199-227: Make configureWithMobileToken functional by replacing
fetchMobileToken’s placeholder completion with an asynchronous request to the
example backend that returns a real mobile token, passing null on failure and
completing exactly once. Update the visible Configure flow to invoke
configureWithMobileToken instead of the publishableApiKey path, while preserving
the existing configuration result handling and one-time token refresh behavior.
🪄 Autofix
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: Enterprise
Run ID: 8686bfbf-cbc2-4590-916a-875f7f0ffe2e
📒 Files selected for processing (1)
app/src/main/java/com/chargebee/example/MainActivity.kt
| private fun configureWithMobileToken() { | ||
| Chargebee.configure( | ||
| site = "cb-abc-test", | ||
| sdkKey = "SDK-KEY", | ||
| packageName = this.packageName, | ||
| tokenProvider = { completion -> | ||
| // Ask your backend for a fresh mobile token (it mints one via | ||
| // `create_mobile_token`), then hand the raw token back to the SDK. | ||
| // Pass null if the token could not be obtained. | ||
| fetchMobileToken(completion) | ||
| } | ||
| ) { | ||
| when (it) { | ||
| is ChargebeeResult.Success -> { | ||
| Log.i(javaClass.simpleName, "Configured with mobile token") | ||
| } | ||
| is ChargebeeResult.Error -> { | ||
| Log.e(javaClass.simpleName, "Configuration failed: ${it.exp.message}") | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /* | ||
| * Stand-in for the call to your own backend that returns a Chargebee mobile token. | ||
| * Replace the body with a real network request to your server. | ||
| */ | ||
| private fun fetchMobileToken(completion: (String?) -> Unit) { | ||
| completion("cb_mob_replace_with_token_from_your_backend") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Make the mobile-token example functional before merge.
fetchMobileToken always returns a placeholder token. Any request using configureWithMobileToken() will fail authentication, including the one-time refresh retry. The visible Configure flow still uses publishableApiKey, so the new path is not integrated into the example. Replace the placeholder with an asynchronous backend request and invoke this configuration path from the example flow.
As per path instructions, this is a functionality-breaking issue that must be resolved before merge.
🤖 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 `@app/src/main/java/com/chargebee/example/MainActivity.kt` around lines 199 -
227, Make configureWithMobileToken functional by replacing fetchMobileToken’s
placeholder completion with an asynchronous request to the example backend that
returns a real mobile token, passing null on failure and completing exactly
once. Update the visible Configure flow to invoke configureWithMobileToken
instead of the publishableApiKey path, while preserving the existing
configuration result handling and one-time token refresh behavior.
Source: Path instructions
CHANGELOG
REPLACE_ME_WITH_CHANGELOG
SUMMARY
REPLACE_ME_WITH_SUMMARY_OF_THE_CHANGES
FUNCTIONAL AUTOMATION CHANGES PR
AUTOMATION TEST REPORT URL
REPLACE_ME_WITH_TEST_REPORT_URL
AREAS OF IMPACT
REPLACE_ME_WITH_AREAS_OF_IMPACT_OR_NA
TYPE OF CHANGE
DOCUMENTATION
REPLACE_ME_WITH_DOCUMENTATION_LINK_OR_NA
Adds mobile-token authentication to the Android SDK. It introduces token-provider configuration, token refresh, dynamic authorization selection, and one retry for eligible 401 responses. It updates the example app with mobile-token configuration support.