# How Authentication Tokens Are Managed and Injected into Requests in GSYGitHubAppCompose

> Learn how GSYGitHubAppCompose manages authentication tokens using Jetpack DataStore and injects them into OkHttp requests via TokenInterceptor for seamless GitHub API access.

- Repository: [Shuyu Guo/gsygithubappcompose](https://github.com/carguo/gsygithubappcompose)
- Tags: how-to-guide
- Published: 2026-02-26

---

**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`](https://github.com/carguo/gsygithubappcompose/blob/main/core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/datastore/UserPreferencesDataStore.kt), the DataStore defines the key and exposes the token stream:

```kotlin
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`](https://github.com/carguo/gsygithubappcompose/blob/main/data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/UserRepository.kt), the login flow persists the token after successful API verification:

```kotlin
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`](https://github.com/carguo/gsygithubappcompose/blob/main/core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/interceptor/TokenInterceptor.kt):

```kotlin
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.

```kotlin
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`](https://github.com/carguo/gsygithubappcompose/blob/main/UserRepository.kt):

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

```

In [`TokenInterceptor.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/TokenInterceptor.kt):

```kotlin
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`](https://github.com/carguo/gsygithubappcompose/blob/main/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`](https://github.com/carguo/gsygithubappcompose/blob/main/core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/di/NetworkModule.kt):

```kotlin
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`](https://github.com/carguo/gsygithubappcompose/blob/main/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.