---
url: /bg/insights/kotlin-jvm-google-sign-in-firebase
title: "Google sign-in for Kotlin/JVM desktop with Firebase — Siere Soft"
description: "How Siere KMP Auth handles Google OAuth on Kotlin/JVM with the system browser, a loopback callback, PKCE and a real Firebase session."
language: bg
canonical: https://sieresoft.com/bg/insights/kotlin-jvm-google-sign-in-firebase
aeo_generated: 2026-09-04T20:07:37.039Z
---

[← Всички статии](/bg/insights)[Kotlin Multiplatform](/bg/insights/topics/kotlin-multiplatform)·Инженерна бележка
# Google sign-in for Kotlin/JVM desktop with Firebase

How Siere KMP Auth handles Google OAuth on Kotlin/JVM with the system browser, a loopback callback, PKCE and a real Firebase session.
TM**Tomislav Mladenov**2 септември 2026 г. · 9 мин четене
Тази статия засега е достъпна на английски.
В тази статия
- [The API I wanted to use](#the-api-i-wanted-to-use)
- [The browser opened, but nothing was listening](#the-browser-opened-but-nothing-was-listening)
- [Before you start](#before-you-start)
- [The complete desktop flow](#the-complete-desktop-flow)
- [Start a loopback server](#start-a-loopback-server)
- [Protect the code with state and PKCE](#protect-the-authorization-code-with-state-and-pkce)
- [Validate the callback](#validate-the-callback)
- [Exchange the code](#exchange-the-authorization-code)
- [Pass the credential to Firebase](#pass-the-google-credential-to-firebase)
- [One more JVM problem](#one-more-jvm-problem-dispatchers-main)
- [A login must survive a restart](#a-login-is-not-complete-until-it-survives-a-restart)
- [What we verified](#what-we-verified)
- [Use it or inspect the implementation](#use-it-or-inspect-the-implementation)

I had already implemented authentication for [Kotlin/JS](https://medium.com/proandroiddev/kotlin-js-auth-part-2-apple-sign-in-ed6d9d9364ac) and [Kotlin/Wasm](https://proandroiddev.com/kotlin-wasm-auth-part-2-apple-sign-in-857326006c64). Every time, the shared authentication API stopped being shared as soon as I reached the platform-specific details.

Desktop Google sign-in was the point where I got tired of solving the same problem inside individual applications.

I wanted one authentication API for Android, iOS, JVM, JS and Wasm, while still allowing each target to use the flow that makes sense on that platform. That became [Siere KMP Auth](https://github.com/SiereSoft/siere-kmp-auth), our open-source authentication library for Kotlin Multiplatform.

This article covers one of the harder parts behind that library: opening Google authentication in the system browser, receiving the result in a Kotlin/JVM desktop application and turning it into a persistent Firebase session.

## The API I wanted to use

Application code should not need to know how to run a local callback server or exchange an OAuth authorization code.

With Siere KMP Auth, the JVM setup looks like this:

`val provider = FirebaseAuthProvider(
    googleAuthConfig = JvmGoogleAuthConfig(
        clientId = desktopClientId,
    ),
)

val auth = SiereAuth(provider)
`
Signing in uses the same shared operation as the other targets:

`when (val result = auth.signInWithGoogle()) {
    is AuthResult.Success -> {
        println("Signed in as ${result.value.displayName}")
    }

    is AuthResult.Failure -> {
        println("Sign-in failed: ${result.error}")
    }
}
`
The JVM implementation handles the browser, callback listener, state validation, PKCE exchange and Firebase credential conversion.

Getting there took more work than the public API suggests.

## The browser opened, but nothing was listening

I expected the browser part to be straightforward. Open Google, let the user authenticate, receive the credential and pass it to Firebase.

Well, the browser opened.

After signing in, Safari redirected to something like:

`http://127.0.0.1:64896/callback
`
Then it displayed:

`Safari Can’t Connect to the Server
`
That page explained the first problem. Opening the browser is only half of a desktop OAuth flow. The desktop application must also run a temporary local HTTP server to receive the authorization response.

## Before you start

This guide does not cover creating a Firebase project, enabling Google authentication or configuring the OAuth consent screen. Firebase and Google already document those steps.

You will need:

- A Firebase project with Google authentication enabled

- The Firebase web configuration for that project

- A Google OAuth client with its application type set to **Desktop app**

- The downloaded OAuth client JSON file

Credential hygiene
Do not commit either configuration file to the repository. The application using the library
should supply its own values.

Google treats installed applications as clients that cannot keep secrets. The downloaded desktop client JSON may contain a `client_secret`, but a distributed desktop application cannot keep that value confidential. [Google explains this limitation in its installed-application OAuth documentation](https://developers.google.com/identity/protocols/oauth2/native-app).

## The complete desktop flow

The implementation has ten steps:

- Generate a random OAuth state value.

- Generate a PKCE verifier and SHA-256 challenge.

- Start an HTTP server on `127.0.0.1` using a random available port.

- Open the Google authorization URL in the system browser.

- Let the user authenticate.

- Receive the authorization code on `/callback`.

- Verify the callback state.

- Exchange the code for Google tokens.

- Create a Firebase Google credential.

- Sign in to Firebase.

There is no embedded WebView and no copy-and-paste step. The user authenticates in their normal browser, and the browser returns the result directly to the desktop application.

## Start a loopback server

Google recommends a loopback redirect for macOS, Windows and Linux desktop applications.

The JDK already includes a small HTTP server that is enough for this callback:

`val address = InetSocketAddress(
    InetAddress.getByName("127.0.0.1"),
    0,
)
val server = HttpServer.create(address, 0)

server.createContext("/callback") { exchange ->
    handleGoogleCallback(exchange)
}

server.start()

val redirectUri = URI(
    "http://127.0.0.1:${server.address.port}/callback",
)
`
Passing `0` as the port asks the operating system to choose an available one.

The server binds explicitly to `127.0.0.1`. Using `localhost` can introduce name-resolution and firewall behaviour that the flow does not need. Google also recommends the literal loopback address for desktop applications.

The server should exist for one authentication attempt and stop as soon as the callback is received, cancelled or timed out.

## Protect the authorization code with state and PKCE

A callback does not become trustworthy merely because it uses `127.0.0.1`.

Each authorization request needs:

- A cryptographically random `state`

- A new PKCE verifier

- An S256 PKCE challenge derived from that verifier

`val state = randomUrlSafe(byteCount = 32)
val verifier = randomUrlSafe(byteCount = 64)

val verifierHash = MessageDigest
    .getInstance("SHA-256")
    .digest(verifier.toByteArray(StandardCharsets.US_ASCII))

val challenge = Base64
    .getUrlEncoder()
    .withoutPadding()
    .encodeToString(verifierHash)
`
The authorization request includes the challenge and state:

`client_id=<desktop-client-id>
redirect_uri=http://127.0.0.1:<port>/callback
response_type=code
scope=openid email profile
code_challenge=<challenge>
code_challenge_method=S256
state=<state>
prompt=select_account
`
The application then opens that request in the system browser:

`Desktop.getDesktop().browse(authorizationUri)
`

## Validate the callback

Once the user finishes authentication, Google redirects the browser to the loopback server.

The callback handler accepts only:

- A `GET` request

- The exact `/callback` path

- A matching state value

- The first valid callback for the current operation

`val parameters = parseQuery(exchange.requestURI.rawQuery)

val validMethod = exchange.requestMethod == "GET"
val validPath = exchange.requestURI.path == "/callback"
val validState = parameters["state"]
    ?.constantTimeEquals(expectedState) == true

val accepted = validMethod &&
    validPath &&
    validState &&
    !callback.isCompleted
`
If the callback is valid, the server returns a small message to the browser:

`Google authorization received. You can return to the application.
`
That message confirms only that the application received the authorization code. Firebase sign-in still has to succeed.

The desktop process must remain alive while the user is in the browser. During my first live attempt, the callback server timed out before I completed the Google flow. The browser reached `127.0.0.1`, but nothing was listening anymore.

The final implementation puts a deadline on the callback and stops the server when the operation succeeds, fails or is cancelled.

## Exchange the authorization code

The callback returns a short-lived authorization code. The application exchanges it at Google’s token endpoint:

`POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded
`
The form body contains:

`client_id=<desktop-client-id>
client_secret=<desktop-client-secret, when required>
code=<authorization-code>
code_verifier=<original-pkce-verifier>
grant_type=authorization_code
redirect_uri=http://127.0.0.1:<port>/callback
`
The `redirect_uri` must match the value used in the authorization request exactly.

Google documents `client_secret` as optional for the desktop token exchange, so Siere KMP Auth omits it by default. Our first live client still responded with:

`invalid_request: client_secret is missing
`
For that client, we read the value from the downloaded desktop OAuth JSON and included it in the exchange. `JvmGoogleAuthConfig` therefore accepts an optional `clientSecret` for compatibility:

`val googleConfig = JvmGoogleAuthConfig(
    clientId = desktopClientId,
    clientSecret = desktopClientSecret,
)
`
That value is still not confidential inside a distributed desktop application. PKCE binds the authorization code to the verifier, `state` rejects unrelated callbacks, and the listener accepts the result only through its temporary loopback address.

## Pass the Google credential to Firebase

The successful token response contains a Google ID token and, depending on the request, an access token.

Those tokens can be converted into a Firebase credential:

`val credential = GoogleAuthProvider.credential(
    idToken = tokens.idToken,
    accessToken = tokens.accessToken,
)

val firebaseUser = Firebase.auth
    .signInWithCredential(credential)
    .user
`
Firebase documents the same mechanism for manually handled Google sign-in: obtain a Google ID token, create a `GoogleAuthProvider` credential and call `signInWithCredential`. [Firebase’s Google authentication guide](https://firebase.google.com/docs/auth/web/google-signin) shows the equivalent web flow.

## One more JVM problem: `Dispatchers.Main`

Once Google OAuth succeeded, the request reached Firebase and failed with:

`Module with the Main dispatcher is missing
`
The JVM Firebase bridge assumed that `Dispatchers.Main` existed.

That assumption works on Android, where the platform provides the main dispatcher. It is unsafe for a headless JVM process and makes testing more difficult.

We fixed it by injecting platform dispatchers:

`interface DispatcherProvider {
    val main: CoroutineDispatcher
    val default: CoroutineDispatcher
    val io: CoroutineDispatcher
    val unconfined: CoroutineDispatcher
}
`
The desktop Firebase bridge can now use the correct dispatcher without hard-coding Android coroutine behaviour. Tests can supply a `TestDispatcherProvider`, while Android and iOS remain free to expose the dispatchers available on their platforms.

The fix touched both GitLive layers used on JVM:

- The [GitLive Firebase Kotlin SDK](https://github.com/GitLiveApp/firebase-kotlin-sdk)

- The underlying [GitLive Firebase Java SDK](https://github.com/GitLiveApp/firebase-java-sdk)

At the time of writing, the upstream Java SDK still describes Firebase Auth as minimal. The credential path we needed was missing, so we implemented it in the Siere forks and tested the Kotlin and Java layers together. We still treat those bridge builds as pre-release until the changes land upstream.

## A login is not complete until it survives a restart

A desktop application has more work after Firebase returns the first user. It must survive a restart, refresh an expired token, link another credential without switching accounts, sign out cleanly and stop pending browser work when the user denies access or closes the application.

The JVM bridge now persists the Firebase user and refresh token through the consumer-provided `FirebasePlatform` storage. Recreating the application restores that session. `getIdToken(true)` forces a refresh, while an expired token refreshes before the caller receives it.

Google linking keeps the current Firebase UID and replaces the stored session only after Firebase accepts the new credential.

The browser flow treats denial as cancellation, puts a deadline on the callback and releases the local server when the calling coroutine or provider is cancelled. Those cases have regression tests because they are the ones most likely to leave a desktop application waiting forever.

The sample uses a file-backed store to prove restart restoration. A production application should store session data in operating-system credential storage rather than a plain file.

## What we verified

The final live run completed the complete flow:

- The system browser opened

- Google authentication succeeded

- The loopback callback was received

- The authorization code was exchanged

- Firebase accepted the Google credential

- The Firebase user was returned

- A Firebase ID token was retrieved

- Sign-out succeeded

The same implementation passed 30 JVM adapter tests and 24 tests in the lower Firebase Java bridge. They cover restart restoration, forced and expired-token refresh, Google linking, sign-out, denial, timeout and cancellation.

No API keys, OAuth files or raw tokens are stored in the repository. During live verification, token output was limited to its length and a SHA-256 digest. That proved a token was returned without leaking it into the logs.

## Use it or inspect the implementation

[Siere KMP Auth is open source on GitHub](https://github.com/SiereSoft/siere-kmp-auth). The public API keeps authentication consistent across Kotlin Multiplatform targets, while the platform implementations handle browser flows, native SDKs and session storage.

This first article focused on Google OAuth and the loopback callback. The next part will cover the less visible work required for a production JVM session: persistence, token refresh, credential linking, cancellation and failure testing.

If your team is building a Kotlin Multiplatform application and authentication is blocking a target, you can [open an issue](https://github.com/SiereSoft/siere-kmp-auth/issues) or [talk to Siere Soft](https://sieresoft.com/contact).

### Да поговорим?

hello@sieresoft.com · Отговаряме до един работен ден, обикновено по-бързо.
[Насрочете 30-минутен разговор →](/bg/contact)[или ни пишете](mailto:hello@sieresoft.com)

## Structured Data

```json
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "СИЕР СОФТ ООД",
  "legalName": "СИЕР СОФТ ООД",
  "url": "https://sieresoft.com",
  "logo": "https://sieresoft.com/icon",
  "email": "hello@sieresoft.com",
  "taxID": "206672583",
  "vatID": "BG206672583",
  "foundingDate": "2021-10-04",
  "founder": {
    "@type": "Person",
    "name": "Peter Manolov"
  },
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "Mladost, bl. 24, entr. A, fl. 2, apt. 3",
    "addressLocality": "Montana",
    "postalCode": "3400",
    "addressCountry": "BG"
  },
  "sameAs": [
    "https://github.com/SiereSoft"
  ]
}
```

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "url": "https://sieresoft.com",
  "name": "Siere Soft",
  "inLanguage": "bg"
}
```

```json
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Siere Soft",
      "item": "https://sieresoft.com/bg"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Статии",
      "item": "https://sieresoft.com/bg/insights"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Google sign-in for Kotlin/JVM desktop with Firebase",
      "item": "https://sieresoft.com/bg/insights/kotlin-jvm-google-sign-in-firebase"
    }
  ]
}
```

```json
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Google sign-in for Kotlin/JVM desktop with Firebase",
  "description": "How Siere KMP Auth handles Google OAuth on Kotlin/JVM with the system browser, a loopback callback, PKCE and a real Firebase session.",
  "datePublished": "2026-09-02",
  "dateModified": "2026-09-02",
  "inLanguage": "en",
  "url": "https://sieresoft.com/bg/insights/kotlin-jvm-google-sign-in-firebase",
  "mainEntityOfPage": "https://sieresoft.com/bg/insights/kotlin-jvm-google-sign-in-firebase",
  "keywords": [
    "Kotlin/JVM",
    "Firebase Auth",
    "Open source"
  ],
  "author": {
    "@type": "Person",
    "name": "Tomislav Mladenov"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Siere Soft",
    "url": "https://sieresoft.com"
  }
}
```

