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