# How GitHub OAuth Authentication Is Implemented in the GSY GitHub App: A Complete Technical Guide

> Learn how the GSY GitHub App implements GitHub OAuth authentication using WebView ViewModel and Repository. Get a complete technical guide to secure GitHub login.

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

---

**The app implements GitHub OAuth authentication through a layered architecture that uses an embedded WebView to capture the authorization code, a ViewModel to manage UI state, and a Repository layer to exchange the code for an access token and persist user credentials locally.**

The `carguo/gsygithubappcompose` repository demonstrates a production-ready implementation of GitHub OAuth authentication using Jetpack Compose and modern Android architecture patterns. This Kotlin-based application separates concerns across UI, networking, and data persistence layers to create a secure and testable login flow.

## Architecture Overview

The GitHub OAuth implementation follows a five-layer architecture that isolates responsibilities from user interaction to data persistence:

- **UI Layer**: Displays the OAuth web page and captures the authorization code
- **WebView Component**: Intercepts the custom scheme redirect from GitHub
- **ViewModel**: Manages authentication state and orchestrates the token exchange
- **Repository**: Handles the code-to-token exchange and local data persistence
- **Network Layer**: Defines Retrofit endpoints for GitHub's OAuth API

## Step-by-Step Authentication Flow

The complete GitHub OAuth authentication flow proceeds through these distinct phases:

1. **User initiates login**: `LoginViewModel.startOAuthFlow()` sets `showOAuthWebView = true`, triggering the UI to display the OAuth screen.
2. **Authorization request**: The app loads `https://github.com/login/oauth/authorize` with the client ID and requested scopes in an embedded WebView.
3. **User consent**: GitHub presents the authorization page; upon approval, GitHub redirects to the custom scheme `gsygithubapp://authed?code=XYZ`.
4. **Code interception**: `OAuthWebView` detects the custom scheme, extracts the `code` query parameter, and invokes the callback.
5. **Token exchange**: `LoginViewModel.handleOAuthCode` forwards the code, client ID, and client secret to `UserRepository.loginWithOAuth`.
6. **Authentication completion**: The repository exchanges the code for an access token, fetches the authenticated user profile, and persists credentials to `UserPreferencesDataStore` and the local database.
7. **Navigation**: On success, the ViewModel updates `isLoggedIn = true`, triggering navigation to the home screen.

## UI Layer: Triggering the OAuth Flow

In [`feature/login/src/main/java/com/shuyu/gsygithubappcompose/feature/login/LoginScreen.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/feature/login/src/main/java/com/shuyu/gsygithubappcompose/feature/login/LoginScreen.kt), the `LoginScreen` composable monitors `uiState.showOAuthWebView` to conditionally display the `OAuthScreen`. This separation keeps the initial login UI distinct from the WebView authorization interface.

When the user taps the GitHub OAuth button, the ViewModel flips the state boolean, causing the UI to render the OAuth screen with the necessary callbacks:

```kotlin
// In LoginScreen.kt
if (uiState.showOAuthWebView) {
    OAuthScreen(
        onCodeReceived = { code ->
            viewModel.handleOAuthCode(
                BuildConfig.CLIENT_ID,
                BuildConfig.CLIENT_SECRET,
                code
            )
        },
        onCancel = { viewModel.cancelOAuthFlow() },
        onError = { viewModel.cancelOAuthFlow() }
    )
}

```

The `OAuthScreen` constructs the GitHub authorization URL using the client credentials stored in `BuildConfig`:

```kotlin
val clientId = BuildConfig.CLIENT_ID
val oauthUrl = "https://github.com/login/oauth/authorize?" +
        "client_id=$clientId&state=app&scope=user,repo,gist,notifications," +
        "read:org,workflow&redirect_uri=gsygithubapp://authed"

```

## WebView Integration: Capturing the Authorization Code

The [`OAuthWebView.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/OAuthWebView.kt) file contains the critical logic for intercepting the OAuth redirect. The custom `WebViewClient` monitors navigation events and detects when the URL matches the app's custom scheme `gsygithubapp://authed`.

When the redirect occurs, the WebView extracts the authorization code from the query parameters:

```kotlin
// OAuthWebView.kt
override fun shouldOverrideUrlLoading(
    view: WebView?, request: WebResourceRequest?
): Boolean {
    val uri = request?.url
    if (uri != null && uri.toString().startsWith("gsygithubapp://authed")) {
        val code = uri.getQueryParameter("code")
        if (code != null) onCodeReceived(code) else onError()
        return true          // Prevent further loading
    }
    return false
}

```

This interception prevents the WebView from attempting to load the custom scheme as a web page, instead capturing the code and passing it back to the ViewModel through the `onCodeReceived` callback.

## ViewModel: Managing Authentication State

The `LoginViewModel` located at [`feature/login/src/main/java/com/shuyu/gsygithubappcompose/feature/login/LoginViewModel.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/feature/login/src/main/java/com/shuyu/gsygithubappcompose/feature/login/LoginViewModel.kt) serves as the intermediary between the UI and repository layers. Its `handleOAuthCode` method initiates the token exchange within a coroutine scope, managing loading states and error handling:

```kotlin
// LoginViewModel.kt
fun handleOAuthCode(clientId: String, clientSecret: String, code: String) {
    viewModelScope.launch {
        try {
            _uiState.update { it.copy(isLoading = true) }
            val result = userRepository.loginWithOAuth(clientId, clientSecret, code)
            result.onSuccess { user ->
                _uiState.update { 
                    it.copy(isLoggedIn = true, isLoading = false, currentUser = user) 
                }
            }.onFailure { error ->
                _uiState.update { it.copy(error = error.message, isLoading = false) }
            }
        } catch (e: Exception) {
            _uiState.update { it.copy(error = e.message, isLoading = false) }
        }
    }
}

```

## Repository: Token Exchange and Persistence

The `UserRepository` class 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) encapsulates the complete authentication business logic. The `loginWithOAuth` function performs three critical operations: exchanging the authorization code for an access token, retrieving the authenticated user profile, and persisting the credentials locally.

```kotlin
// UserRepository.kt
suspend fun loginWithOAuth(
    clientId: String, 
    clientSecret: String, 
    code: String
): Result<User> {
    return try {
        // 1️⃣ Exchange code for access token
        val tokenResponse = apiService.getAccessToken(clientId, clientSecret, code)
        val accessToken = tokenResponse.accessToken

        // 2️⃣ Fetch authenticated user with bearer token
        val authHeader = "token $accessToken"
        val user = apiService.getAuthenticatedUser(authHeader)

        // 3️⃣ Persist credentials to DataStore and local DB
        preferencesDataStore.saveAuthToken(accessToken)
        preferencesDataStore.saveUsername(user.login)
        preferencesDataStore.saveUserId(user.id.toString())
        userDao.insertUser(user.toEntity())

        Result.success(user)
    } catch (e: Exception) {
        Result.failure(e)
    }
}

```

This implementation uses `UserPreferencesDataStore` for secure token storage and a Room database (`userDao`) for caching the user entity, ensuring offline access to user information.

## Network Layer: GitHub API Endpoints

The Retrofit service definition in [`core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/api/GitHubApiService.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/api/GitHubApiService.kt) declares the OAuth token exchange endpoint. Unlike typical API methods that use relative paths, this endpoint requires the full GitHub URL:

```kotlin
// GitHubApiService.kt
@POST("https://github.com/login/oauth/access_token")
@Headers("Accept: application/json")
suspend fun getAccessToken(
    @Query("client_id") clientId: String,
    @Query("client_secret") clientSecret: String,
    @Query("code") code: String
): AccessToken

```

The `@Headers("Accept: application/json")` annotation ensures GitHub returns a JSON response rather than the default form-encoded format, enabling automatic deserialization into the `AccessToken` data class.

## Summary

- **Layered Architecture**: The implementation separates concerns across UI, ViewModel, Repository, and Network layers for maintainability and testability.
- **Custom Scheme Handling**: The WebView intercepts `gsygithubapp://authed` redirects to extract the authorization code without external browser dependencies.
- **Secure Token Exchange**: The Repository handles the server-to-server code exchange using Retrofit, never exposing the client secret to the UI layer.
- **Local Persistence**: Access tokens and user profiles are stored in DataStore and Room database, enabling persistent login sessions across app restarts.
- **Configuration Management**: Client credentials are injected via `BuildConfig`, keeping secrets out of source control while allowing different configurations for debug and release builds.

## Frequently Asked Questions

### How does the app handle the OAuth redirect without leaving the application?

The app uses an embedded `WebView` with a custom `WebViewClient` that overrides `shouldOverrideUrlLoading`. When the URL starts with the custom scheme `gsygithubapp://authed`, the WebView intercepts the navigation, extracts the `code` parameter, and invokes the callback rather than attempting to load the URL. This keeps the entire authentication flow within the app's native interface.

### Where are the GitHub client ID and secret stored securely?

The client credentials are stored in `BuildConfig` fields (`BuildConfig.CLIENT_ID` and `BuildConfig.CLIENT_SECRET`), which are typically injected through `local.properties` or environment variables during the build process. According to the source code in [`LoginScreen.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/LoginScreen.kt), these values are passed to the ViewModel only when needed for the token exchange, and the Repository handles the actual API call, keeping sensitive operations away from the UI layer.

### What scopes does the app request during GitHub authorization?

The authorization URL constructed in `OAuthScreen` requests the following scopes: `user`, `repo`, `gist`, `notifications`, `read:org`, and `workflow`. These permissions allow the app to access user profiles, repositories, gists, notifications, organization membership, and GitHub Actions workflows, providing full functionality for a comprehensive GitHub client application.

### How is the access token persisted across app sessions?

After successful authentication, the `UserRepository` saves the access token to `UserPreferencesDataStore` using `preferencesDataStore.saveAuthToken(accessToken)`. The username and user ID are also stored separately. This DataStore implementation provides asynchronous, transactional storage that survives app restarts, allowing the app to maintain authenticated sessions without requiring users to log in repeatedly.