How Authentication Tokens Are Managed and Injected into Requests in GSYGitHubAppCompose

The GSYGitHubAppCompose project stores GitHub authentication tokens in Jetpack DataStore, caches them in memory via TokenInterceptor, and automatically injects the Authorization: token <token> header into every OkHttp request without manual header handling in API calls.

Managing authentication state in Android applications requires secure persistence and seamless request injection. In the carguo/gsygithubappcompose repository, the app handles GitHub API authentication using a layered approach that combines Jetpack DataStore for persistence, a custom OkHttp Interceptor for header injection, and coroutines for reactive token updates. This architecture ensures that once a user logs in, every subsequent network request automatically carries valid credentials.

Secure Token Persistence with Jetpack DataStore

The foundation of the authentication system is UserPreferencesDataStore, which implements IUserPreferencesDataStore to abstract storage operations. The token is stored under the auth_token key using stringPreferencesKey, and exposed as a reactive Flow<String?> that other components can observe.

In core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/datastore/UserPreferencesDataStore.kt, the DataStore defines the key and exposes the token stream:

private object PreferencesKeys {
    val AUTH_TOKEN = stringPreferencesKey("auth_token")
}

override val authToken: Flow<String?> = context.dataStore.data
    .map { preferences -> preferences[PreferencesKeys.AUTH_TOKEN] }

Saving Tokens After Authentication

When a user logs in via UserRepository.login() or loginWithOAuth(), the verified token is persisted using saveAuthToken(). The repository also stores associated metadata like username and user ID for session management.

In data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/UserRepository.kt, the login flow persists the token after successful API verification:

suspend fun login(token: String): Result<User> {
    val user = apiService.getAuthenticatedUser("token $token")
    preferencesDataStore.saveAuthToken(token)   // Persisted to DataStore
    preferencesDataStore.saveUsername(user.login)
    preferencesDataStore.saveUserId(user.id.toString())
    return Result.success(user)
}

Automatic Header Injection via TokenInterceptor

The TokenInterceptor class is the core component that manages how authentication tokens are injected into requests. Provided as a singleton to the OkHttp client, it maintains an in-memory cache using MutableStateFlow<String?> to avoid synchronous disk reads on every request, significantly improving performance.

Reactive Token Caching

Upon initialization, the interceptor launches a coroutine that collects the authToken Flow from the DataStore, keeping the _token StateFlow updated with the latest value. This reactive approach ensures the interceptor always has the current token without blocking the main thread.

In core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/interceptor/TokenInterceptor.kt:

private val _token = MutableStateFlow<String?>(null)

init {
    interceptorScope.launch {
        userPreferencesDataStore.authToken.collect { newToken ->
            _token.value = newToken               // Keep in-memory copy updated
        }
    }
}

Request Interception Logic

In the intercept() method, the interceptor checks the in-memory cache first. If the token is null (cache miss), it synchronously reads from DataStore using runBlocking { first() }, updates the cache, and then constructs the request with the Authorization header.

override fun intercept(chain: Interceptor.Chain): Response {
    var request = chain.request()
    var currentToken = _token.value

    if (currentToken == null) {
        currentToken = runBlocking { userPreferencesDataStore.authToken.first() }
        _token.value = currentToken
    }

    currentToken?.let { token ->
        request = request.newBuilder()
            .header("Authorization", "token $token")
            .build()
    }
    return chain.proceed(request)
}

Login and Logout Lifecycle Management

The UserRepository orchestrates the authentication lifecycle. During login, it verifies the token with the GitHub API before persisting it. During logout, it clears both the DataStore and the interceptor's memory cache to ensure no stale tokens remain.

Clearing Authentication State

The logout() method in UserRepository calls preferencesDataStore.clearAll() to remove the token from persistent storage. Additionally, the UI layer can trigger tokenInterceptor.clearAuthorization() to immediately purge the in-memory cache and stop header injection for subsequent requests.

In UserRepository.kt:

suspend fun logout() {
    preferencesDataStore.clearAll()   // Removes token from DataStore
    appDatabase.clearAllData()
}

In TokenInterceptor.kt:

fun clearAuthorization() {
    interceptorScope.launch {
        userPreferencesDataStore.clearAuthToken()
        _token.value = null
    }
}

Global Interceptor Registration in NetworkModule

To ensure every request benefits from automatic authentication, the TokenInterceptor is registered globally in NetworkModule.kt. This wires the interceptor into the OkHttp client used by GitHubApiService, making token injection transparent to all API call sites.

In core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/di/NetworkModule.kt:

fun provideOkHttpClient(tokenInterceptor: TokenInterceptor): OkHttpClient {
    return OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .addInterceptor(tokenInterceptor)   // Token added globally to all requests
        .build()
}

Summary

  • Tokens are stored in Jetpack DataStore under the auth_token key and exposed as Flow<String?> for reactive observation
  • TokenInterceptor maintains an in-memory cache via MutableStateFlow to minimize disk I/O on every request
  • The interceptor falls back to synchronous DataStore reads using runBlocking { first() } when the cache is empty
  • The Authorization header follows the format token <token> and is automatically added to every request
  • Logout clears both DataStore (clearAll()) and the interceptor's cache (clearAuthorization())
  • Global registration in NetworkModule ensures seamless injection across all GitHubApiService calls

Frequently Asked Questions

How does the app handle token updates without restarting?

The TokenInterceptor collects the DataStore Flow in its init block, so when saveAuthToken() writes a new value, the in-memory _token StateFlow updates automatically. Subsequent requests use the new token immediately without requiring an app restart or manual refresh.

What happens if the DataStore token is cleared while requests are in flight?

The interceptor's clearAuthorization() method sets the in-memory cache to null immediately. While in-flight requests may still use the cached token, any new requests will check the cache, find it null, read the now-empty DataStore via first(), and proceed without adding the Authorization header, effectively terminating authenticated access.

Why does the interceptor use runBlocking to read from DataStore?

OkHttp interceptors operate synchronously on the request thread. Since DataStore exposes asynchronous Flow APIs, runBlocking { authToken.first() } allows the interceptor to safely fetch the current token value synchronously when the in-memory cache is null, ensuring the request can proceed with proper headers without violating OkHttp's threading model.

Where is the TokenInterceptor registered in the dependency injection graph?

In NetworkModule.kt, the provideOkHttpClient() function receives TokenInterceptor as a constructor-injected parameter and registers it via .addInterceptor(tokenInterceptor). This ensures every request made through the generated GitHubApiService Retrofit interface automatically includes the authentication header.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →