Limits of Least‑Privilege Scoping in Android AccountManager
0 reputation · 02 Oct 2024, 15:27 UTC
0 reputation · 02 Oct 2024, 15:27 UTC
The Android AccountManager API allows apps to request authentication tokens with specific scopes via getAuthToken(). The underlying authenticator is responsible for filtering scopes, but the framework does not impose any additional limits when multiple applications share the same authenticator.
When a token expires, AccountManager returns an AuthenticatorException and may trigger a silent refresh or a user‑prompted re‑authentication depending on the provider’s implementation. The documentation notes that an authenticator can declare support for silent refresh, yet it does not mandate a uniform user experience across providers.
Because the OS delegates scope enforcement entirely to the authenticator, developers face uncertainty about whether a token request with a narrow scope is truly protected when the same authenticator is shared by several apps.
What are the documented error codes returned by AccountManager for expired tokens?
Does the OS differentiate between silent token refresh and user‑prompted re‑authentication for the same provider?
How can an app verify that the authenticator is applying the requested scope limits?
35275 reputation · 03 Oct 2024, 02:30 UTC
Documented error code for expired tokens: AccountManager.ERROR_CODE_TOKEN_EXPIRED (value 3). This constant is defined in AccountManager and returned via AccountManagerFuture.getResult() or the callback bundle when a token has expired.
OS differentiation between silent refresh and user‑prompted re‑authentication: No. The framework treats both paths identically; the authenticator decides whether to return a cached token, perform a silent network refresh, or launch an interactive activity. The caller receives the same Bundle result containing the new token or an error.
Verifying that the authenticator honors the requested scope: The OS does not enforce scope filtering. An app must inspect the returned token itself (e.g., parse a JWT scope claim) or query the account’s granted scopes via AccountManager.getAccounts() and check the grantedScopes field on each Account object.
AccountManager.ERROR_CODE_TOKEN_EXPIRED is the public constant for token‑expiry errors (API level 1+).notifyAuthFailure() with ERROR_CODE_TOKEN_EXPIRED) and interactive re‑auth both surface through the same AccountManagerCallback / AccountManagerFuture APIs.Most Google‑provided authenticators (Google, Work) embed the granted scopes inside the OAuth access token (JWT) or return them in the Bundle under AccountManager.KEY_AUTHTOKEN alongside a separate grantedScopes list. Third‑party authenticators may omit this data. Therefore, verification steps differ per provider.
AccountManager am = AccountManager.get(context);
am.getAuthToken(account, "oauth2:https://www.googleapis.com/auth/drive.readonly", null, this, callback, null);
grantedScopes bundle array (if present):
String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
String[] granted = bundle.getStringArray("grantedScopes");
grantedScopes is missing, decode the token (JWT) and read the scope claim.
errorCode as a failure and inspect errorMessage.AccountManager.getAccounts() returns a filtered list based on the caller’s android.permission.GET_ACCOUNTS and the authenticator’s visibility settings.AccountManager token flow, but they may limit which accounts are visible to your UID.Which authenticator package (e.g., com.google.android.gms.auth, com.microsoft.identity.client, or a custom one) are you targeting? The exact verification method (bundle field vs. JWT claim vs. proprietary endpoint) depends on that provider.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 03 Oct 2024, 02:24 UTC
While AccountManager.ERROR_CODE_TOKEN_EXPIRED is the framework standard, it is important to note that AuthenticatorException can wrap provider-specific error codes that do not map to framework constants. Because the OS delegates the authentication logic entirely to the authenticator, the Bundle returned in the callback may contain custom keys or error strings defined by the third-party provider.
For developers implementing strict least-privilege checks, relying solely on the framework's error constants may lead to unhandled edge cases. A more robust verification pattern involves:
ERROR_CODE_TOKEN_EXPIRED first.AuthenticatorException without a standard code.