This guide explains how to integrate the Firebase Auth KMP library into your Kotlin Multiplatform project.
Firebase Auth KMP is a Kotlin Multiplatform library that provides Firebase Authentication for Android and iOS with a unified API.
Group ID: dev.com3run
Artifact ID: firebase-auth-kmp
Version: 1.0.0
If you've published the library to Maven Local for testing:
In your project's settings.gradle.kts:
dependencyResolutionManagement {
repositories {
mavenLocal() // Add this
google()
mavenCentral()
}
}In your module's build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.com3run:firebase-auth-kmp:1.0.0")
}
}
}Once published to Maven Central, no need to add mavenLocal(), just add the dependency:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.com3run:firebase-auth-kmp:1.0.0")
}
}
}Download google-services.json from Firebase Console and place it in your composeApp/ or Android module directory.
Add the Google Services plugin in build.gradle.kts:
plugins {
// ... other plugins
id("com.google.gms.google-services") version "4.4.2" apply false
}In your app module build.gradle.kts:
plugins {
// ... other plugins
id("com.google.gms.google-services")
}The library needs a reference to the current Activity for Google Sign-In. In your MainActivity:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import dev.com3run.firebaseauthkmp.ActivityHolder
import dev.com3run.firebaseauthkmp.GoogleSignInInterop
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set activity reference for library
ActivityHolder.current = this
setContent {
// Your app content
}
}
override fun onDestroy() {
super.onDestroy()
ActivityHolder.current = null
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
GoogleSignInInterop.onActivityResult(requestCode, resultCode, data)
}
}Download GoogleService-Info.plist from Firebase Console and add it to your iOS app in Xcode.
Add Firebase SDK using CocoaPods or Swift Package Manager.
Using CocoaPods (Recommended):
Create Podfile in iosApp/ directory:
platform :ios, '13.0'
target 'iosApp' do
use_frameworks!
pod 'FirebaseAuth', '~> 10.0'
endThen run:
cd iosApp
pod installUsing Swift Package Manager:
In Xcode, go to File → Add Packages → Enter Firebase URL:
https://github.com/firebase/firebase-ios-sdk
Select FirebaseAuth and add to your target.
Create FirebaseAuthBridge.swift in your iOS app:
import Foundation
import FirebaseAuth
import AuthenticationServices
class FirebaseAuthBridge {
static let shared = FirebaseAuthBridge()
private let center = NotificationCenter.default
private var appleSignInCoordinator: AppleSignInCoordinator?
private init() {}
func start() {
// Listen for auth requests from Kotlin
center.addObserver(
forName: NSNotification.Name("AuthRequest"),
object: nil,
queue: .main
) { [weak self] notification in
self?.handleAuthRequest(notification)
}
// Listen for Apple Sign-In requests
center.addObserver(
forName: NSNotification.Name("AppleSignInRequest"),
object: nil,
queue: .main
) { [weak self] notification in
self?.handleAppleSignInRequest(notification)
}
// Monitor auth state changes
Auth.auth().addStateDidChangeListener { [weak self] _, user in
self?.postAuthState(user: user)
}
}
private func handleAuthRequest(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let requestId = userInfo["requestId"] as? String,
let action = userInfo["action"] as? String else {
return
}
Task {
do {
let user: User?
switch action {
case "anonymous":
let result = try await Auth.auth().signInAnonymously()
user = result.user
case "signUpWithEmailAndPassword":
guard let email = userInfo["email"] as? String,
let password = userInfo["password"] as? String else {
postAuthError(requestId: requestId, code: "invalid-params", message: "Missing email or password")
return
}
let result = try await Auth.auth().createUser(withEmail: email, password: password)
user = result.user
case "signInWithEmailAndPassword":
guard let email = userInfo["email"] as? String,
let password = userInfo["password"] as? String else {
postAuthError(requestId: requestId, code: "invalid-params", message: "Missing email or password")
return
}
let result = try await Auth.auth().signIn(withEmail: email, password: password)
user = result.user
case "google":
guard let idToken = userInfo["idToken"] as? String else {
postAuthError(requestId: requestId, code: "invalid-params", message: "Missing ID token")
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken, accessToken: nil)
let result = try await Auth.auth().signIn(with: credential)
user = result.user
case "apple":
guard let idToken = userInfo["idToken"] as? String else {
postAuthError(requestId: requestId, code: "invalid-params", message: "Missing ID token")
return
}
let credential = OAuthProvider.credential(
withProviderID: "apple.com",
idToken: idToken,
rawNonce: nil
)
let result = try await Auth.auth().signIn(with: credential)
user = result.user
case "signOut":
try Auth.auth().signOut()
user = nil
default:
postAuthError(requestId: requestId, code: "unsupported-action", message: "Action not supported: \\(action)")
return
}
postAuthSuccess(requestId: requestId, user: user)
} catch {
postAuthError(requestId: requestId, code: "auth-error", message: error.localizedDescription)
}
}
}
@objc private func handleAppleSignInRequest(_ notification: Notification) {
if #available(iOS 13.0, *) {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
let coordinator = AppleSignInCoordinator { [weak self] idToken in
self?.postAppleSignInResult(idToken: idToken)
}
authorizationController.delegate = coordinator
authorizationController.presentationContextProvider = coordinator
self.appleSignInCoordinator = coordinator
authorizationController.performRequests()
} else {
print("❌ Apple Sign-In requires iOS 13.0 or later")
postAppleSignInResult(idToken: nil)
}
}
private func postAuthSuccess(requestId: String, user: User?) {
var responseInfo: [String: Any] = [
"requestId": requestId,
"status": "success"
]
if let user = user {
responseInfo["uid"] = user.uid
responseInfo["email"] = user.email ?? ""
responseInfo["displayName"] = user.displayName ?? ""
responseInfo["photoUrl"] = user.photoURL?.absoluteString ?? ""
responseInfo["isAnonymous"] = user.isAnonymous
responseInfo["isEmailVerified"] = user.isEmailVerified
}
center.post(name: NSNotification.Name("AuthResponse"), object: nil, userInfo: responseInfo)
}
private func postAuthError(requestId: String, code: String, message: String) {
let responseInfo: [String: Any] = [
"requestId": requestId,
"status": "error",
"errorCode": code,
"errorMessage": message
]
center.post(name: NSNotification.Name("AuthResponse"), object: nil, userInfo: responseInfo)
}
private func postAuthState(user: User?) {
var userInfo: [String: Any] = [:]
if let user = user {
userInfo["uid"] = user.uid
userInfo["email"] = user.email ?? ""
userInfo["displayName"] = user.displayName ?? ""
userInfo["photoUrl"] = user.photoURL?.absoluteString ?? ""
userInfo["isAnonymous"] = user.isAnonymous
userInfo["isEmailVerified"] = user.isEmailVerified
} else {
userInfo["uid"] = ""
}
center.post(name: NSNotification.Name("AuthState"), object: nil, userInfo: userInfo)
}
private func postAppleSignInResult(idToken: String?) {
let userInfo: [String: Any] = ["idToken": idToken as Any]
center.post(name: NSNotification.Name("AppleSignInCompleted"), object: nil, userInfo: userInfo)
}
}
@available(iOS 13.0, *)
private class AppleSignInCoordinator: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
private let completion: (String?) -> Void
init(completion: @escaping (String?) -> Void) {
self.completion = completion
super.init()
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential,
let identityTokenData = appleIDCredential.identityToken,
let idToken = String(data: identityTokenData, encoding: .utf8) else {
print("❌ Failed to get identity token from Apple Sign-In")
completion(nil)
return
}
print("✅ Apple sign-in successful")
completion(idToken)
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
print("❌ Apple sign-in error: \\(error.localizedDescription)")
completion(nil)
}
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return UIApplication.shared
.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow } ?? UIWindow()
}
}In your AppDelegate.swift or app entry point:
import Firebase
import UIKit
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
FirebaseAuthBridge.shared.start()
return true
}
}
@main
struct YourApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}In Xcode:
- Select your target
- Go to "Signing & Capabilities"
- Click "+ Capability"
- Add "Sign in with Apple"
import dev.com3run.firebaseauthkmp.AuthRepository
import dev.com3run.firebaseauthkmp.platformAuthBackend
// Create auth repository
val authRepository = AuthRepository(platformAuthBackend())
// Listen to auth state
authRepository.authState.collect { user ->
if (user != null) {
println("Signed in as: ${user.displayName ?: user.uid}")
} else {
println("Signed out")
}
}val result = authRepository.signUpWithEmailAndPassword(
email = "user@example.com",
password = "securePassword123"
)
when (result) {
is AuthResult.Success -> println("Account created!")
is AuthResult.Failure -> println("Error: ${result.error}")
}val result = authRepository.signInWithEmailAndPassword(
email = "user@example.com",
password = "securePassword123"
)
when (result) {
is AuthResult.Success -> println("Signed in!")
is AuthResult.Failure -> println("Error: ${result.error}")
}val result = authRepository.signInAnonymously()
when (result) {
is AuthResult.Success -> println("Signed in as guest!")
is AuthResult.Failure -> println("Error: ${result.error}")
}// Request ID token using platform-specific flow
val idToken = requestGoogleIdToken()
if (idToken != null) {
val result = authRepository.signInWithGoogle(idToken)
when (result) {
is AuthResult.Success -> println("Signed in with Google!")
is AuthResult.Failure -> println("Error: ${result.error}")
}
}// Check if available (iOS only)
if (isAppleSignInAvailable()) {
val idToken = requestAppleIdToken()
if (idToken != null) {
val result = authRepository.signInWithApple(idToken)
when (result) {
is AuthResult.Success -> println("Signed in with Apple!")
is AuthResult.Failure -> println("Error: ${result.error}")
}
}
}authRepository.signOut()val result = authRepository.sendPasswordResetEmail("user@example.com")
when (result) {
is AuthResult.Success -> println("Reset email sent!")
is AuthResult.Failure -> println("Error: ${result.error}")
}Using Koin:
import org.koin.dsl.module
import dev.com3run.firebaseauthkmp.*
val authModule = module {
single<AuthBackend> { platformAuthBackend() }
single { AuthRepository(get()) }
}The library provides detailed error types:
when (result) {
is AuthResult.Success -> {
// Handle success
}
is AuthResult.Failure -> {
when (result.error) {
AuthError.InvalidCredential -> println("Invalid credentials")
AuthError.UserNotFound -> println("User not found")
AuthError.WrongPassword -> println("Wrong password")
AuthError.EmailAlreadyInUse -> println("Email already in use")
AuthError.WeakPassword -> println("Password too weak")
AuthError.InvalidEmail -> println("Invalid email format")
is AuthError.Network -> println("Network error: ${result.error.message}")
is AuthError.Unknown -> println("Unknown error: ${result.error.message}")
else -> println("Error: ${result.error}")
}
}
}The library includes a FakeAuthBackend for testing:
import dev.com3run.firebaseauthkmp.FakeAuthBackend
import dev.com3run.firebaseauthkmp.AuthRepository
val fakeBackend = FakeAuthBackend()
val authRepository = AuthRepository(fakeBackend)
// Use in tests- Make sure
google-services.jsonis in the correct location - Verify SHA-1 fingerprint is added to Firebase Console
- Check that
ActivityHolder.currentis set in MainActivity - Ensure Google Sign-In is enabled in Firebase Console
- Verify "Sign in with Apple" capability is added in Xcode
- Check that
FirebaseAuthBridge.shared.start()is called - Ensure
GoogleService-Info.plistis added to the project - Verify Apple Sign-In is enabled in Firebase Console
If you get "cannot access" errors, make sure you've added the library dependency in the correct sourceSet (commonMain).
Check out the composeApp module in this repository for a complete working example.
For issues and questions:
- GitHub Issues: https://github.com/com3run/firebase-auth-kmp/issues
- Documentation: See LIBRARY_DOCUMENTATION.md
MIT License