← Back to Blog

Mastering Kotlin 2.4+ SwiftPM (Part 3): Implementing Firebase Authentication in KMP

Learn how to implement Firebase Authentication in a Kotlin Multiplatform (KMP) project, enabling you to share authentication logic between Android and iOS using SwiftPM.

AG Mobile Labs10 min read
AndroidiOSKMPFirebaseAuthAndroid StudioFirebaseSwift Package ManagerKotlin
Kotlin Multiplatform (KMP) Firebase Authentication SwiftPM Setup
In the previous article, we learned how to configure Firebase in our KMP project and make it available across our common code source sets. In this part, we will build upon our setup to implement Firebase Authentication for both Android and iOS apps.

Initializing Firebase

To initialize Firebase for Android, we need to call the FirebaseApp.initializeApp() method within the onCreate() method of our custom Application class. Here, I created AuroraApplication inside the androidApp module and registered it in the androidApp/src/main/AndroidManifest.xml file.
// androidApp/src/main/kotlin/com/ai/auroraai/AuroraApplication.kt
import android.app.Application
import com.google.firebase.FirebaseApp
 
class AuroraApplication: Application() {
    override fun onCreate() {
        super.onCreate()
        FirebaseApp.initializeApp(this)
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
    <application
        android:name=".AuroraApplication"
        <!-- Existing activity and other declarations -->
    </application>
 
</manifest>
For iOS, we can initialize Firebase by calling FIRApp.configure() inside the MainViewController located in the iosMain source set.
// shared/src/iosMain/kotlin/com/ai/auroraai/MainViewController.kt
fun MainViewController() = ComposeUIViewController {
    FIRApp.configure()
    App()
}
As you may have noticed, SwiftPM significantly simplifies iOS Firebase configuration. There is no longer a need to write Swift code in the iosApp module just to configure Firebase! This is a massive improvement over the older CocoaPods approach.

Implementing Firebase Authentication

Now that we've set up Firebase and added the necessary dependencies, let's dive into implementing the authentication flow. We'll create a shared authentication interface named AuthManager in the shared/commonMain source set, which we will then implement for each platform.
// shared/src/commonMain/kotlin/com/ai/auroraai/domain/AuthManager.kt
interface AuthManager {
    fun signIn(email: String, password: String, onResult: (Boolean, String?) -> Unit)
}
This shared interface defines the contract for our authentication operations. In androidMain, we provide the Android-specific implementation for this interface:
// shared/src/androidMain/kotlin/com/ai/auroraai/data/AndroidAuthManager.kt
class AndroidAuthManager : AuthManager {
    private val auth: FirebaseAuth
        get() = FirebaseAuth.getInstance()
 
    override fun signIn(
        email: String,
        password: String,
        onResult: (Boolean, String?) -> Unit
    ) {
        auth.signInWithEmailAndPassword(email, password)
            .addOnCompleteListener { task ->
                if (task.isSuccessful) {
                    onResult(true, null)
                } else {
                    onResult(false, task.exception?.localizedMessage ?: "Authentication failed")
                }
            }
    }
}
Similarly, we define the iOS implementation for AuthManager in the shared/iosMain source set:
// shared/src/iosMain/kotlin/com/ai/auroraai/data/IOSAuthManager.kt
class IosAuthManager : AuthManager {
    @OptIn(ExperimentalForeignApi::class)
    override fun signIn(
        email: String,
        password: String,
        onResult: (Boolean, String?) -> Unit
    ) {
        FIRAuth.auth().signInWithEmail(email, password = password) { result, error ->
            if (error != null) {
                onResult(false, error.localizedDescription)
            } else {
                onResult(true, null)
            }
        }
    }
}
While we could use expect/actual for the AuthManager, using a standard Kotlin interface combined with dependency injection (DI) or factory patterns is widely considered best practice for business logic and third-party services like Firebase for several reasons:

Flexibility vs Rigidity

  • The expect/actual approach (Rigid): If you mark expect interface AuthManager or expect class AndroidAuthManager, you are telling the compiler: "There is a physical, platform-specific implementation that will be compiled directly into the binary for each target." The problem here is that you can't easily swap out the implementation for testing or mock data. If you are in commonMain writing tests for your ViewModel, you are stuck using the actual native Firebase implementation, which requires a real device or simulator to run.
  • The interface approach (Flexible): By using a standard common interface, your common code doesn't care where the implementation comes from.
    • It could come from androidMain (using Firebase Android)
    • It could come from iosMain (using Firebase iOS)
    • It could come from a commonMain mock class used entirely for local preview screens or unit testing.

Cleaner Architecture (Dependency Injection)

When you use an interface, you pass the implementation into your common business logic at runtime. This decouples your code completely.
For example, your ViewModel in commonMain can look like this:
class LoginViewModel(private val authManager: AuthManager) {
    fun handleLogin() {
        authManager.signIn("user@test.com", "password") { success, error ->
            // Update UI state completely in commonMain
        }
    }
}
A good rule of thumb in modern KMP development is:
  • Use expect/actual for low-level platform APIs, system utilities, or hardware configurations (e.g., getting the device OS version, accessing file system paths, handling the platform Context object).
  • Use interface for business logic, network clients, databases, and third-party wrappers (like Firebase, Supabase, or Analytics).
To get the platform-specific implementations into common code, we can use a simple Factory pattern or a DI library like Koin.
Since we haven't configured Koin in our project yet, we will use a Simple Factory pattern for now:
Under commonMain, we declare the createAuthManager expect function inside the AuthManagerFactory.kt file.
// shared/src/commonMain/kotlin/com/ai/auroraai/data/AuthManagerFactory.kt
import com.ai.auroraai.domain.AuthManager
 
expect fun createAuthManager(): AuthManager
Next, we provide the actual Android and iOS implementations under androidMain and iosMain.
// shared/src/androidMain/kotlin/com/ai/auroraai/data/AuthManagerFactory.kt
import com.ai.auroraai.domain.AuthManager
 
actual fun createAuthManager(): AuthManager {
    return AndroidAuthManager()
}
// shared/src/iosMain/kotlin/com/ai/auroraai/data/AuthManagerFactory.kt
import com.ai.auroraai.domain.AuthManager
 
actual fun createAuthManager(): AuthManager {
    return IosAuthManager()
}

Create and Use the ViewModel in commonMain

Because LoginViewModel lives entirely in commonMain, it only interacts with the AuthManager interface. It doesn't know (or care) whether it's executing Android Play Services or an iOS Swift Package under the hood.
// shared/src/commonMain/kotlin/com/ai/auroraai/presentation/LoginViewModel.kt
package com.ai.auroraai.presentation
 
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ai.auroraai.domain.AuthManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
 
class LoginViewModel(private val authManager: AuthManager): ViewModel() {
    private val _uiState = MutableStateFlow<String>("Idle")
    val uiState: StateFlow<String> = _uiState
 
    fun login(email: String, password: String) {
        _uiState.value = "Loading..."
 
        viewModelScope.launch {
            authManager.signIn(email, password) { success, errorMessage ->
                if (success) {
                    _uiState.value = "Success! Logged in."
                } else {
                    _uiState.value = "Error: ${errorMessage ?: "Unknown error"}"
                }
            }
        }
    }
}
Since our project shares Compose UI across both platforms, we can instantiate LoginViewModel in the shared module and use it on both platforms seamlessly. In our case, we haven't created a custom UI for the login screen yet. To verify that our setup works correctly, we will create an instance of LoginViewModel in the App.kt file within commonMain. We'll also add a minimal UI to test if our Firebase implementation authenticates users successfully.
// shared/src/commonMain/kotlin/com/ai/auroraai/presentation/App.kt
 
@Composable
@Preview
fun App() {
    val loginViewModel = remember {
        LoginViewModel(authManager = createAuthManager())
    }
    val loginState by loginViewModel.uiState.collectAsStateWithLifecycle()
    MaterialTheme {
        Column(
            modifier = Modifier
                .background(MaterialTheme.colorScheme.primaryContainer)
                .safeContentPadding()
                .fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Text("Auth Status: $loginState")
 
            Button(onClick = {
                loginViewModel.login("example@gmail.com", "123456")
            }) {
                Text("Sign In")
            }
 
        }
    }
}
In the Firebase console, I have already added a test user with the email "example@gmail.com" and the password "123456".
I ran the app on an Android emulator and an iOS simulator. After clicking the Sign In button, it displays a successful authentication state, as shown in the screenshots below:
Android Firebase AuthiOS Firebase Auth

Source Code

You can find the complete, working source code for this project—including the Firebase and SwiftPM configuration—on my GitHub repository: 👉 AuroraAi-kmp on GitHub

Conclusion & Next Steps

We have successfully implemented Firebase Authentication in our Kotlin Multiplatform project using the interface pattern! This approach not only keeps our commonMain code clean and testable but also takes full advantage of Swift Package Manager on the iOS side, avoiding any unnecessary Swift bindings.
In the next part of this series, we will look into adding Dependency Injection (like Koin) to properly inject our AuthManager and scale our architecture, as well as building a beautiful, fully functional Login and Signup UI using Compose Multiplatform.
Stay tuned! If you have any questions, suggestions, or run into issues setting this up, please feel free to open an issue on the GitHub repository.

Enjoyed this article?

Subscribe to my newsletter to get the latest articles on Android architecture, KMP, and mobile engineering straight to your inbox.