The question
In multi-application ecosystems—whether enterprise suites, interactive classroom hardware, or related consumer apps—users expect to sign in once.
On Android, each application is strictly sandboxed inside its own Linux user ID and private storage directory (/data/data/<package_name>). App A cannot peek into App B’s SharedPreferences or database.
So the engineering question becomes:
How do several distinct APKs on a single device agree on who is signed in, refresh credentials safely, and stay in sync—without each app maintaining its own brittle copy of the truth?
My initial mental model
Early in my career, my first instinct was to share authentication tokens through an exported ContentProvider or external storage:
[App A] ──┐
├─► Shared ContentProvider / Encrypted File ──► Stored Token
[App B] ──┘
This model breaks down almost immediately:
- Security Vulnerabilities: An exported
ContentProvideror shared storage location exposes credentials to eavesdropping by untrusted third-party apps on the device unless guarded by strict signature permissions. - Desynchronized Invalidation: If App A discovers the access token has expired and fetches a new pair, App B still holds the dead token.
- The Stampeding Refresh Stampede: If five apps start simultaneously, all five detect an expired token and issue parallel refresh calls to your backend API, triggering token rotation failures.
What actually happens: Native AccountManager
Android provides a dedicated operating system framework specifically designed for system-wide account coordination: AccountManager backed by an AbstractAccountAuthenticator service.
System Service: AccountManagerService
▲
│ Binder IPC
┌──────────────────────────────┴──────────────────────────────┐
│ │
[Client App: Whiteboard] [Client App: EduAI]
Calls: getAuthToken() Calls: getAuthToken()
│ │
└──────────────────────────────┬──────────────────────────────┘
│ Delegated IPC
▼
[Host App: Authenticator Service]
- Manages secure account credentials
- Negotiates OAuth token refresh
- Serializes network requests
Instead of each app managing its own network client and token cache:
- The Host APK implements an
AccountAuthenticatorservice registered in the Android manifest with anandroid.accounts.AccountAuthenticatorXML metadata resource. - The Client APKs do not know or care how credentials are authenticated. They simply ask
AccountManager: “Give me a valid auth token for account typecom.example.account.”
Let’s look at the implementation
1. Hardening with Signature Permissions
To prevent malicious apps from querying user accounts, declare a custom permission with protectionLevel="signature" in your Host APK:
<!-- Host Manifest -->
<permission
android:name="com.example.auth.PERMISSION_MANAGE_ACCOUNTS"
android:protectionLevel="signature" />
<service
android:name=".authenticator.CustomAccountAuthenticatorService"
android:exported="true"
android:permission="com.example.auth.PERMISSION_MANAGE_ACCOUNTS">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
Because protectionLevel="signature" is enforced at the Linux kernel and package manager level, Android only grants this permission to apps signed with your identical release keystore.
2. The Transparent Auth Token Loop
In client applications, requesting tokens is completely agnostic of token expiration logic:
// Client application code
class SessionTokenProvider(private val context: Context) {
private val accountManager = AccountManager.get(context)
suspend fun getValidBearerToken(account: Account): String = withContext(Dispatchers.IO) {
val future = accountManager.getAuthToken(
account,
"FULL_ACCESS",
null, // options bundle
false, // notifyAuthFailure
null, null // callback, handler
)
val result = future.result
result.getString(AccountManager.KEY_AUTHTOKEN)
?: throw IllegalStateException("Token retrieval failed")
}
fun invalidateStaleToken(accountType: String, token: String) {
accountManager.invalidateAuthToken(accountType, token)
}
}
If the token is cached in AccountManagerService, it returns in microseconds. If invalidateAuthToken was called after a 401 Unauthorized, AccountManager automatically wakes up the Host Authenticator Service to perform the OAuth refresh transparently.
The surprising part: Identity vs. Active Profile
One key insight is that cryptographic identity and runtime UI profile are two distinct problems:
- Authentication Token (Identity): Cryptographic keys, refresh tokens, and OAuth scopes. These are durable, change infrequently, and belong strictly in
AccountManager. - Active Profile Context (Runtime State): Which student profile, classroom section, or grade level is currently active on screen.
Attempting to store rapid, ephemeral profile changes inside AccountManager.setUserData() creates heavy disk write and IPC overhead.
The cleaner solution is to treat AccountManager as the source of identity, and use a lightweight signature-guarded broadcast channel or ContentObserver for propagating live profile context changes across active apps.
Practical consequences for Android developers
1. Handling Global Account Removal
When an administrator logs out of the device, the host authenticator calls accountManager.removeAccountExplicitly().
Every client app should register an OnAccountsUpdateListener:
accountManager.addOnAccountsUpdatedListener({ accounts ->
val hasValidAccount = accounts.any { it.type == "com.example.account" }
if (!hasValidAccount) {
// Purge local app caches and redirect to welcome screen
sessionRepository.clearUserData()
navigateToLogin()
}
}, Handler(Looper.getMainLooper()), true)
This guarantees instantaneous, synchronized logout across all five applications simultaneously without polling.
What I would remember
- Leverage the platform: Don’t build bespoke AIDL singletons for authentication when
AccountManagerhas solved system-wide account coordination since API level 5. - Signature permissions are your security perimeter: They provide rock-solid inter-app authorization with zero runtime permission dialogs.
- Decouple identity from active state: Store credentials in
AccountManager, but keep dynamic UI context in lightweight reactive pub/sub streams.
Further reading
- AOSP Source:
frameworks/base/core/java/android/accounts/AccountManager.java - Android Developers Guide: Creating a Custom Account Authenticator