In this post, I will share my experience implementing Firebase Google Login across four platforms—Android, iOS, JVM (Desktop), and JS—in a Kotlin Multiplatform project, including the challenges I faced and how I overcame them.
Background
While Firebase provides native SDKs for JS, iOS, and Android, it does not yet officially support KMP. As a workaround, the community-led GitLive Firebase SDK is widely used for KMP support. However, its feature set is not yet complete. Specifically, the JVM platform has a very minimal implementation for the Auth API, forcing us to manually call the Firebase REST API.
Here is a high-level look at the implementation strategy for each platform:
- Android: Acquire Google ID Token via Credential Manager API → Authenticate with Firebase using GitLive SDK.
- iOS: Acquire Google ID Token via Swift Google SDK → Authenticate with Firebase using GitLive SDK.
-
JS: Handle everything at once using the Firebase Web SDK's
signInWithPopup. - JVM: Acquire Google ID Token via OAuth 2.0 REST API → Authenticate via Firebase REST API.
Core Concepts
Authentication Flow
The overall flow for Firebase Google Login is as follows:
- User clicks "Sign in with Google"
-
Perform Platform-Specific Google Auth
- Android/iOS/JVM: Retrieve Google ID Token.
- JS: Complete both Google and Firebase Auth using the Firebase Web SDK.
-
Return Auth Result
-
Android/iOS/JVM:
GoogleSignInResult.Credential(containsidToken). -
JS:
GoogleSignInResult.SignedInUser(contains Firebase user info).
-
Android/iOS/JVM:
-
Firebase Authentication
-
Android/iOS/JVM: Call
signInWithCredentialvia GitLive SDK. -
JS: Skip (already handled internally by
signInWithPopup).
-
Android/iOS/JVM: Call
Session Creation: Store session info in local storage.
UI Update: Display user information.
Tokens: Google vs. Firebase
It is crucial to understand the difference between the two tokens used in this process:
| Feature | Google ID Token | Firebase ID Token |
|---|---|---|
| Issuer | Google OAuth 2.0 | Firebase Authentication |
| Purpose | Proof of identity to Firebase | Communication with Firebase backend |
| Format | JWT (Signed by Google) | JWT (Signed by Firebase) |
Platform-Specific Breakdown
| Platform | Google Auth Method | Firebase Auth Method |
|---|---|---|
| Android | Credential Manager API | GitLive SDK (signInWithCredential) |
| iOS | Swift Google SDK | GitLive SDK (signInWithCredential) |
| Web | Firebase Web SDK | Internal to signInWithPopup
|
| JVM | OAuth 2.0 REST API | Firebase REST API + Manual Injection |
Token Management: While GitLive SDK automatically manages tokens on Android, iOS, and JS, the JVM implementation requires manually setting the token in the FirebasePlatform due to SDK limitations.
Configuration
Common Setup
Add the GitLive SDK dependency to your commonMain source set:
implementation("dev.gitlive:firebase-auth:1.12.0")
Platform-Specific Dependencies
Android:
implementation("androidx.credentials:credentials:1.2.2")
implementation("androidx.credentials:credentials-play-services-auth:1.2.2")
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0")
iOS (via Swift Package Manager):
- Firebase iOS SDK
- Google Sign-In SDK
JVM:
implementation("io.ktor:ktor-client-core:2.3.7")
implementation("io.ktor:ktor-client-cio:2.3.7")
implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
Implementation
Since the method for acquiring the Google ID Token varies by platform, I abstracted the logic using a GoogleAuthenticator interface.
interface GoogleAuthenticator {
suspend fun signInWithGoogle(): Result<GoogleSignInResult>
}
sealed class GoogleSignInResult {
data class Credential(val idToken: String, val accessToken: String?) : GoogleSignInResult()
data class SignedInUser(val user: User) : GoogleSignInResult()
}
Android Implementation
Acquiring Google ID Token (Credential Manager API):
val googleIdOption = GetGoogleIdOption.Builder()
.setServerClientId(YOUR_WEB_CLIENT_ID)
.setFilterByAuthorizedAccounts(false)
.build()
val request = GetCredentialRequest.Builder()
.addGoogleIdOption(googleIdOption)
.build()
val result = credentialManager.getCredential(activity, request)
val idToken = GoogleIdTokenCredential.createFrom(result.credential).idToken
Firebase Authentication:
val credential = FirebaseGoogleAuthProvider.credential(idToken, null)
FirebaseAuth.auth.signInWithCredential(credential)
iOS Implementation (Swift Interop)
For iOS, I defined a GoogleSignInProvider interface to bridge Swift and Kotlin. Swift implements this interface, and Kotlin calls it.
Kotlin Interface Definition:
interface GoogleSignInProvider {
fun getGoogleCredential(
onSuccess: (idToken: String, accessToken: String?) -> Unit,
onFailure: (String) -> Unit
)
}
// Global reference for the Swift implementation
private var googleSignInProvider: GoogleSignInProvider? = null
fun setGoogleSignInProvider(provider: GoogleSignInProvider) {
googleSignInProvider = provider
}
Swift Implementation:
class IOSGoogleSignInProvider: NSObject, GoogleSignInProvider {
func getGoogleCredential(
onSuccess: @escaping (String, String?) -> Void,
onFailure: @escaping (String) -> Void
) {
GIDSignIn.sharedInstance.signIn(withPresenting: topVC) { result, error in
guard let idToken = result?.user.idToken?.tokenString else {
onFailure("Failed to retrieve Google ID Token")
return
}
onSuccess(idToken, nil)
}
}
}
Calling Swift from Kotlin:
class IosGoogleAuthenticator : GoogleAuthenticator {
override suspend fun signInWithGoogle(): Result<GoogleSignInResult.Credential> {
return suspendCancellableCoroutine { continuation ->
getGoogleSignInProvider()?.getGoogleCredential(
onSuccess = { idToken, accessToken ->
continuation.resume(Result.success(GoogleSignInResult.Credential(idToken, accessToken)))
},
onFailure = { error ->
continuation.resume(Result.failure(Exception(error)))
}
)
}
}
}
Web (JS) Implementation
Leveraging the Firebase Web SDK's interoperability via dynamic types:
val authModule = js("require('firebase/auth')")
val firebaseAuth: dynamic = authModule.getAuth()
val provider = js("new authModule.GoogleAuthProvider()")
val resultPromise: Promise<dynamic> = authModule.signInWithPopup(firebaseAuth, provider)
// Handle promise result and map to User domain model...
JVM (Desktop) Implementation
OAuth 2.0 Authorization Code Flow:
This involves starting a local callback server to intercept the auth code from the system browser.
// 1. Generate Auth URL and open system browser
val authUrl = "https://accounts.google.com/o/oauth2/v2/auth?..."
Desktop.getDesktop().browse(URI(authUrl))
// 2. Wait for callback and exchange code for ID Token via Ktor
val googleIdToken = httpClient.post("https://oauth2.googleapis.com/token") { ... }.body<TokenResponse>().idToken
Direct Injection into FirebasePlatform:
Since GitLive's JVM implementation is minimal, we exchange the Google ID Token for a Firebase Token via REST and manually store it in the SDK's internal key.
val key = "com.google.firebase.auth.FIREBASE_USER"
val userJson = """{ "uid": "$uid", "idToken": "$firebaseToken", ... }"""
platform.store(key, userJson)
⚠️ Warning: This approach relies on the internal implementation of the GitLive SDK and may break with future updates.
Summary
By combining the Firebase SDK, GitLive SDK, and platform-specific Google Auth SDKs, I successfully implemented Firebase Authentication for all four platforms in a KMP project. While the JVM platform required hacky workarounds due to SDK limitations, abstracting the logic with the GoogleAuthenticator interface allowed me to keep the business logic and UI layer clean and shared in Kotlin.
Top comments (0)