๐Ÿ”Œ APIs, Databases, and Backend Integration

Secure Token Storage and API Authentication

Access and refresh tokens are the keys to a user's account. Stored carelessly in plain SharedPreferences, logs or backups, they can be stolen and replayed. Here's a practical, current approach to storing tokens and handling API authentication on Android.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Advanced
On this page
  1. Threat model first
  2. What about EncryptedSharedPreferences?
  3. Implementation: encrypted token store
  4. Exclude secrets from backup
  5. Token lifecycle best practices
  6. Variations
  7. Common mistakes
  8. Frequently asked questions
  9. Conclusion

On Android, your app's private storage is sandboxed from other apps. That's a strong baseline. But tokens can still leak through device backups, rooted or compromised devices, debug logs, or analytics. Defence in depth means encrypting secrets with keys the app can't export, and limiting what a stolen token can do.

Threat model first

ThreatMitigation
Other apps reading your filesApp sandbox (default); never store tokens on shared storage
Tokens extracted from backupsExclude token files from backup; encrypt with non-exportable Keystore keys
Rooted device or malware with rootKeystore-backed encryption raises the bar; short-lived access tokens limit damage
Leaks via logs, crash reports, analyticsRedact Authorization headers; never log tokens
Stolen refresh tokenServer-side rotation and revocation; device binding where available
Envelope encryption of tokens with Android Keystore A master key lives inside the Android Keystore, hardware-backed where available, and never leaves it. It encrypts a data-encryption keyset managed by Tink. The keyset encrypts tokens, and the ciphertext is stored in DataStore in app-private storage. The app decrypts tokens in memory when needed. Android Keystoremaster key (non-exportable) wraps Tink keysetAES-GCM AEAD encrypts DataStore fileciphertext only, no backup
Envelope encryption: even if the file is copied off the device, it's useless without the Keystore key that never leaves the phone.

What about EncryptedSharedPreferences?

For years, Jetpack Security's EncryptedSharedPreferences was the standard answer. The androidx.security:security-crypto library has since been deprecated. It still works in existing apps, but new code should use an alternative, typically DataStore with Tink-based encryption and a Keystore-wrapped key, and existing apps should plan a migration.

Implementation: encrypted token store

app/build.gradle.ktsGradle KTS
dependencies {
    implementation("androidx.datastore:datastore:<version>")
    implementation("com.google.crypto.tink:tink-android:<version>")
}
TokenCrypto.ktKotlin
/** AEAD backed by a Tink keyset that is itself encrypted with an Android Keystore master key. */
object TokenCrypto {
    private const val KEYSET_NAME = "token_keyset"
    private const val PREF_FILE = "token_keyset_prefs"
    private const val MASTER_KEY_URI = "android-keystore://token_master_key"

    fun aead(context: Context): Aead {
        AeadConfig.register()
        return AndroidKeysetManager.Builder()
            .withSharedPref(context, KEYSET_NAME, PREF_FILE)       // stores the ENCRYPTED keyset
            .withKeyTemplate(KeyTemplates.get("AES256_GCM"))
            .withMasterKeyUri(MASTER_KEY_URI)                      // Keystore key wraps the keyset
            .build()
            .keysetHandle
            .getPrimitive(RegistryConfiguration.get(), Aead::class.java)
    }
}
TokenStore.ktKotlin
@Serializable
data class AuthTokens(val accessToken: String, val refreshToken: String, val expiresAtEpochSec: Long)

class EncryptedTokensSerializer(private val aead: Aead) : Serializer<AuthTokens?> {
    override val defaultValue: AuthTokens? = null
    private val associatedData = "auth_tokens".toByteArray()

    override suspend fun readFrom(input: InputStream): AuthTokens? {
        val bytes = input.readBytes()
        if (bytes.isEmpty()) return null
        return runCatching {
            Json.decodeFromString<AuthTokens>(aead.decrypt(bytes, associatedData).decodeToString())
        }.getOrNull()                                       // undecryptable (key lost) โ†’ treat as signed out
    }

    override suspend fun writeTo(t: AuthTokens?, output: OutputStream) {
        if (t == null) return                               // empty file = no tokens
        output.write(aead.encrypt(Json.encodeToString(t).encodeToByteArray(), associatedData))
    }
}

class TokenStore(context: Context) {
    private val store = DataStoreFactory.create(
        serializer = EncryptedTokensSerializer(TokenCrypto.aead(context)),
        produceFile = { File(context.filesDir, "datastore/auth_tokens.enc") },
    )
    @Volatile private var cached: AuthTokens? = null       // fast synchronous reads for interceptors

    val tokens: Flow<AuthTokens?> = store.data.onEach { cached = it }

    fun accessTokenBlocking(): String? = cached?.accessToken
    suspend fun save(tokens: AuthTokens) { store.updateData { tokens } }
    suspend fun clear() { store.updateData { null } }
}

AndroidX has also introduced a dedicated DataStore + Tink integration artifact. Check its status (it was alpha at the time of writing) before adopting it instead of a custom serializer. Initialise the cache at startup, for example by collecting tokens in an application scope, before the first authenticated request.

Exclude secrets from backup

res/xml/data_extraction_rules.xml (Android 12+)XML
<data-extraction-rules>
    <cloud-backup>
        <exclude domain="file" path="datastore/auth_tokens.enc" />
        <exclude domain="sharedpref" path="token_keyset_prefs.xml" />
    </cloud-backup>
    <device-transfer>
        <exclude domain="file" path="datastore/auth_tokens.enc" />
        <exclude domain="sharedpref" path="token_keyset_prefs.xml" />
    </device-transfer>
</data-extraction-rules>

Reference it with android:dataExtractionRules in the manifest, and provide equivalent fullBackupContent rules for Android 11 and lower. Keystore keys aren't backed up, so restored ciphertext would be undecryptable anyway, but excluding it avoids confusing states.

Token lifecycle best practices

  • Short-lived access tokens (minutes), plus a refresh token with server-side rotation: each refresh issues a new refresh token and invalidates the old one.
  • Refresh in one place: an OkHttp Authenticator. See OkHttp Interceptors.
  • Logout clears everything: tokens, cached user data, and a server-side revocation call.
  • Treat decryption failure as signed out. Keys can be invalidated, for example after some device security changes.

Variations

Banking-grade apps

Add Keystore keys requiring user authentication (biometric) for high-risk operations, plus Play Integrity checks on the server.

Firebase Auth

Firebase manages its own session storage. Request fresh ID tokens via the SDK rather than storing them yourself.

OAuth / OpenID Connect

Use AppAuth with PKCE in a Custom Tab for the login flow, then store the resulting tokens as above.

Common mistakes

  • Plain SharedPreferences or DataStore for tokens.
  • Hard-coded encryption keys in the APK, which aren't secret.
  • Logging tokens or full request headers.
  • Long-lived access tokens with no rotation.
  • Storing passwords on the device instead of tokens.

Frequently asked questions

Is the app sandbox not enough?

It protects against other apps on a healthy device. Encryption adds protection for backups, device transfers and some compromised-device scenarios.

Can a rooted device still steal tokens?

A sufficiently privileged attacker can read memory while the app runs. Encryption raises the cost, and short-lived tokens plus server-side checks limit the damage.

Should I store tokens in the Keystore directly?

The Keystore stores keys, not arbitrary data. Store encrypted tokens elsewhere, using a Keystore key for encryption.

Conclusion

Store tokens encrypted with a non-exportable Android Keystore key (for example DataStore plus Tink), exclude them from backups, keep access tokens short-lived with rotating refresh tokens, refresh in one place, and never log them. Migrate away from the deprecated EncryptedSharedPreferences.

Next step: protect secrets beyond tokens in Secure API Keys and Secrets in Android Apps.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Security is easiest to get right when it is part of how you build from the first screen. Strengthen the Android fundamentals that secure, privacy-respecting apps are built on.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp