@W-22701996: [Android] Improve error handling at token refresh#4
Conversation
…ce token error type and handle user_blocked_retry without logout)
…Review: Collapse multi-line log statements)
…Review: Extract inline strings to constants)
…Review: Use static imports for error constants)
…Review: Static import LogoutReason)
…Review: Add final modifier to local variables)
…Review: Restore Javadoc to ATTESTATION constant)
…Review: Add doc comments to new constants)
…Review: Add doc comments to broadcast extra constants)
…Review: Static import OAuth2 members)
…Review: Add final to loop-local variable)
…Review: Static import LogoutReason members)
…Review: Remove redundant assignment flagged by inspector)
…Review: Consolidate duplicate error bundle creation)
…Review: Hoist accounts/matchingAccount to method scope to eliminate duplicate lookup)
…Review: Guard invalidateToken against null after user_blocked_retry)
…uthenticatorService tests for error-specific token refresh behavior)
…Review: Rename EXTRA_TOKEN_ERROR_TYPE to EXTRA_TOKEN_ERROR)
…Review: Rearrange imports)
…Review: Restore revoke API comment on logout call)
…Review: Extract test helper to reduce boilerplate in token error tests)
…Review: Remove unused imports in AuthenticatorServiceTest)
…Review: Extract test helper to reduce boilerplate in AuthenticatorServiceTest)
| public static final String ACCESS_TOKEN_REFRESH_INTENT = "access_token_refeshed"; | ||
| public static final String INSTANCE_URL_UPDATE_INTENT = "instance_url_updated"; | ||
| /** Intent extra: the {@code error} value from the token endpoint response (e.g. "user_blocked", "invalid_grant"). */ | ||
| public static final String EXTRA_TOKEN_ERROR = "token_error"; |
| ofe.getHttpStatusCode() + ")", ofe); | ||
| } | ||
| return null; | ||
| } catch (OAuthFailedException ofe) { |
There was a problem hiding this comment.
Requirement #1 (enabler): Previously this method caught OAuthFailedException, logged, and returned null — discarding all error context. Now it rethrows so that getNewAuthToken() can inspect the specific error type and branch accordingly.
| final String errorType = tokenError != null ? tokenError.error : null; | ||
| final String errorDesc = tokenError != null ? tokenError.errorDescription : null; | ||
|
|
||
| if (!USER_BLOCKED_RETRY_ERROR.equals(errorType)) { |
There was a problem hiding this comment.
Requirement #4: user_blocked_retry is retriable — the server couldn't verify attestation but the refresh token is still valid. We skip logout here so the app can re-attest and retry. All other errors (including user_blocked) are terminal and trigger logout.
Requirement #3: user_blocked uses LogoutReason.USER_BLOCKED instead of REFRESH_TOKEN_EXPIRED so apps/analytics can distinguish "blocked by attestation" from "token expired".
| SalesforceSDKLogger.i(TAG, "Token endpoint error: (Error: " + ofe.response.error + ", Status Code: " + ofe.httpStatusCode + ")", ofe); | ||
|
|
||
| // Terminal errors (except retriable attestation) redirect to login. | ||
| if (!USER_BLOCKED_RETRY_ERROR.equals(ofe.response.error) && ofe.isRefreshTokenInvalid()) { |
There was a problem hiding this comment.
Requirement #4: For the AuthenticatorService path, user_blocked_retry must NOT redirect to login. Since isRefreshTokenInvalid() only checks HTTP status codes (400/401/403) and user_blocked_retry returns 400, we check the error type first. The combined condition ensures retriable errors return an error bundle while terminal errors still redirect to login.
| @@ -236,6 +242,7 @@ public enum LogoutReason { | |||
| UNEXPECTED, // Unexpected error or crash | |||
| UNEXPECTED_RESPONSE, // Unexpected response from server | |||
| UNKNOWN, // Unknown | |||
There was a problem hiding this comment.
Requirement #3: USER_BLOCKED provides a distinct logout reason for attestation failures, differentiating from REFRESH_TOKEN_EXPIRED. Available to SalesforceSDKManager.logout() callers and analytics/telemetry.
Generated by 🚫 Danger |
Summary
Surfaces specific token endpoint error types (
user_blocked,user_blocked_retry,invalid_grant) to apps via broadcast intent extras during token refresh, and handlesuser_blocked_retrywithout logging out the user.Work Item: W-22701996 — [Android] Improve error handling at token refresh
Problem
Previously, all token endpoint errors (HTTP 400/401/403) during refresh were treated identically — the SDK logged out the user and broadcast
ACCESS_TOKEN_REVOKE_INTENTwith no error details. For App Attestation, the server returns two distinct error types:user_blocked(terminal — device/app failed integrity checks)user_blocked_retry(retriable — attestation couldn't be verified but the refresh token is still valid)Both incorrectly triggered logout. Apps had no way to distinguish error types to show appropriate UI.
Changes
OAuth2.java: AddedUSER_BLOCKED_ERRORandUSER_BLOCKED_RETRY_ERRORconstants. AddedUSER_BLOCKEDtoLogoutReasonenum.ClientManager.java: AddedEXTRA_TOKEN_ERRORandEXTRA_TOKEN_ERROR_DESCRIPTIONbroadcast intent extra constants. RestructuredgetNewAuthToken()to catchOAuthFailedExceptionand branch on error type —user_blocked_retryskips logout while terminal errors use the appropriateLogoutReason. All OAuth failures broadcast error details in intent extras. ModifiedrefreshStaleToken()to rethrowOAuthFailedExceptioninstead of swallowing it.AuthenticatorService.java: Added early check foruser_blocked_retryto return an error bundle without redirecting to login, before theisRefreshTokenInvalid()check that would otherwise treat it as terminal.Tests Added
AuthenticatorServiceTest.kt(new file, 3 tests):user_blocked_retry→ returns error bundle (no login redirect)user_blocked→ redirects to logininvalid_grant→ redirects to loginClientManagerMockTest.kt(3 new tests):user_blocked→ logout withUSER_BLOCKEDreason, extras in broadcastuser_blocked_retry→ NO logout, null token returned, extras in broadcastinvalid_grant→ logout withREFRESH_TOKEN_EXPIRED, extras in broadcastHow Tests Validate Requirements
EXTRA_TOKEN_ERRORandEXTRA_TOKEN_ERROR_DESCRIPTIONon captured broadcast intentsuser_blocked→ logout with specific reasontestGetNewAuthToken_UserBlockedverifiesLogoutReason.USER_BLOCKEDpassed tologout()user_blocked_retry→ no logout, fail waiting requeststestGetNewAuthToken_UserBlockedRetryverifieslogout()never called, null token returnedTest plan
ClientManagerMockTesttests pass (11 pre-existing + 3 new)AuthenticatorServiceTesttests pass (3 new)