androidengineers.Book a session

Security Architecture

Exercise: Secure Secrets & Tokens

exercise55 minHard

Audit and fix the security of a real authentication flow: find hardcoded secrets, migrate token storage to encrypted preferences, and implement automatic token refresh.

Starting State: Insecure Code

// ❌ Multiple security violations in production code

// AuthConfig.kt — hardcoded secrets
object AuthConfig {
    const val CLIENT_ID = "abc123"
    const val CLIENT_SECRET = "supersecret_xyz"  // VIOLATION 1: hardcoded secret in APK
    const val API_KEY = "api-prod-key-456"        // VIOLATION 2: hardcoded in APK
}

// TokenManager.kt — unencrypted storage
class TokenManager(context: Context) {
    private val prefs = context.getSharedPreferences("auth", Context.MODE_PRIVATE)  // VIOLATION 3: unencrypted

    var accessToken: String?
        get() = prefs.getString("access_token", null)
        set(value) { prefs.edit().putString("access_token", value).apply() }

    var refreshToken: String?
        get() = prefs.getString("refresh_token", null)
        set(value) { prefs.edit().putString("refresh_token", value).apply() }
}

// LoginViewModel.kt — logging token
class LoginViewModel : ViewModel() {
    fun login(email: String, password: String) {
        viewModelScope.launch {
            val token = api.login(email, password, AuthConfig.CLIENT_SECRET)
            Log.d("Auth", "Token: $token")  // VIOLATION 4: token in logcat
            tokenManager.accessToken = token
        }
    }
}

Step 1: Remove Hardcoded CLIENT_SECRET

Client secrets don't belong in mobile apps. Replace with PKCE:

// AuthConfig.kt — no secrets
object AuthConfig {
    const val CLIENT_ID = "abc123"
    // No client secret — use PKCE
    const val AUTHORIZATION_ENDPOINT = "https://auth.example.com/authorize"
    const val TOKEN_ENDPOINT = "https://auth.example.com/token"
    const val REDIRECT_URI = "com.example.app://oauth/callback"
}

For the API_KEY, fetch it from your backend after authentication:

// On successful login, server issues a session token that scopes API access
// API key never travels to the client — server adds it server-side
class AuthenticatedApiRepository(
    private val sessionToken: String,
    private val api: ProtectedApi
) {
    // No API_KEY needed — bearer token authorizes access
    suspend fun getArticles() = api.getArticles()  // session token in OkHttp Interceptor
}

Step 2: Encrypt Token Storage

// TokenManager.kt — encrypted
class TokenManager(context: Context) {
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    private val prefs = EncryptedSharedPreferences.create(
        context,
        "secure_auth",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

    var accessToken: String?
        get() = prefs.getString("access_token", null)
        set(value) { prefs.edit().putString("access_token", value ?: "").apply() }

    var refreshToken: String?
        get() = prefs.getString("refresh_token", null)
        set(value) { prefs.edit().putString("refresh_token", value ?: "").apply() }

    var tokenExpiry: Long
        get() = prefs.getLong("token_expiry", 0)
        set(value) { prefs.edit().putLong("token_expiry", value).apply() }

    fun isValid() = accessToken != null && System.currentTimeMillis() < tokenExpiry - 60_000

    fun clear() = prefs.edit().clear().apply()
}

Step 3: Fix Logging

// Remove ALL token logging
class LoginViewModel(
    private val authRepository: AuthRepository,
    private val tokenManager: TokenManager
) : ViewModel() {
    fun login(email: String, password: String) {
        viewModelScope.launch {
            try {
                val response = authRepository.login(email, password)
                tokenManager.accessToken = response.accessToken
                tokenManager.refreshToken = response.refreshToken
                tokenManager.tokenExpiry = System.currentTimeMillis() + response.expiresInMs
                // ✅ No token logging — just state
                _state.value = LoginState.Success
            } catch (e: Exception) {
                Timber.e(e, "Login failed")  // Timber + release tree strips this in production
                _state.value = LoginState.Error(e.message ?: "Login failed")
            }
        }
    }
}

Step 4: Implement Automatic Token Refresh

class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .header("Authorization", "Bearer ${tokenManager.accessToken}")
            .build()
        return chain.proceed(request)
    }
}

class TokenRefreshAuthenticator(
    private val tokenManager: TokenManager,
    private val authApi: AuthApi
) : Authenticator {
    private val refreshLock = ReentrantLock()

    override fun authenticate(route: Route?, response: Response): Request? {
        if (response.code != 401) return null

        // Ensure only one refresh happens at a time
        refreshLock.withLock {
            // Check if another request already refreshed the token
            if (tokenManager.isValid()) {
                return response.request.newBuilder()
                    .header("Authorization", "Bearer ${tokenManager.accessToken}")
                    .build()
            }

            val refreshToken = tokenManager.refreshToken ?: return null

            return try {
                val newTokens = authApi.refreshToken(refreshToken).execute().body() ?: return null
                tokenManager.accessToken = newTokens.accessToken
                tokenManager.refreshToken = newTokens.refreshToken
                tokenManager.tokenExpiry = System.currentTimeMillis() + newTokens.expiresInMs

                response.request.newBuilder()
                    .header("Authorization", "Bearer ${newTokens.accessToken}")
                    .build()
            } catch (e: IOException) {
                null  // refresh failed; user will need to log in again
            }
        }
    }
}

Verification Checklist

[ ] grep -r "CLIENT_SECRET" src/ — must return 0 results
[ ] grep -r "getSharedPreferences" src/ — review each; none should store tokens
[ ] grep -r "Log.d.*[Tt]oken" src/ — must return 0 results
[ ] Run APK analyzer (Build → Analyze APK) → classes.dex → search for literal secrets
[ ] adb logcat | grep -i "token" while logging in — must return 0 results
[ ] Test token refresh: set expiry to now - 1; make an API call; verify refresh happened

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Exercise: Secure Secrets & Tokens | Android System Design | Android Engineers