Building an App with Kotlin Multiplatform: Building our Authentication module

B

In the last post we outlined the foundations for how our kotlin multiplatform project is going to be structured. With this in mind, we’re going to start building the next part of our project – here we’ll start with the remote layer of our application.

You may recall seeing how we had each API responsibility separated into an individual module – whilst this may not suit every application, it allows me to keep things separated for future plans (building multi-platform libraries) and also helps to explain things better during this series.

With one of these modules containing only the remote authentication logic, we’re going to go ahead and create/configure a Firebase Authentication wrapper within its own module. This Authentication Remote module will be then used by our Shared Authentication module to communicate with remote authentication services.

Note: The full source for this module can be found on GitHub here.


We’re going to begin by creating a new module in our project – this module is just going to be a pure multiplatform module. For this we’ll add the multiplatform plugin along with the kotlin-serialization plugin as this will be used for object serialization.

plugins {
    kotlin("multiplatform")
    id("kotlinx-serialization")
}

Next we’ll declare some of the configurations for the different platforms that we’ll be targeting. As we’re currently only supporting iOS and Android, we’ll go ahead and add the following:

kotlin {
    val iOSTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
        if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
            ::iosArm64
        else
            ::iosX64
    iOSTarget("ios") {
        binaries {
            framework {
                baseName = "BaseRemote"
            }
        }
    }
    jvm("android")
}

You’re going to see the above code a few times throughout this series, but we’ll go through it here to gain a better understanding of what is going on here. To begin with, we start by calculating the iOSTarget platform that is in use – this depends on the Xcode environment variables when running our code in Xcode. Once we have this target value we can then declare the source use (and name) within our project using iOSTarget(“ios”). Within the binaries/framework clauses we then set the baseName property – this is the name of our exported framework.

When it comes to Android we don’t need to do any of the above, we just declare that we are using a jvm source with the name android using jvm(“android”). Now that we’ve defined sources we can go ahead and define the dependencies that are going to be used by each source. We’ll begin by adding the common dependencies for our module. You may have noticed that above we did not define a common source, this is already declared with the use of the multiplatform plugin that we applied to our module.

For the dependencies of the project I have declared these within the buildSrc module of the project, you can find this file here. I don’t want to spend too much time here diving into what each dependency does and how it’s used, we’ll try to cover more for each one as we come to using it.

For each source we’re going to use the following block for declaring dependencies, where commonMain here is the name of the source – this will change depending on the source being used:

sourceSets["commonMain"].dependencies { }

The commonMain source is the multiplatform code within our module that does not contain any native implementations. For this module we won’t be implementing anything natively, if we were then we would need to create corresponding sources for android and iOS.

For our commonMain source we’ll go ahead and add the required dependencies:

sourceSets["commonMain"].dependencies {
    implementation(Deps.Kotlin.common)
    implementation(Deps.Kotlin.kotlinSerialization)
    implementation(Deps.Kotlin.stdLib)
    implementation(Deps.Ktor.clientCore)
    implementation(Deps.Ktor.clientJson)
    implementation(Deps.Ktor.clientSerialization)
}

The kotlin dependency provides us access to the kotlin framework, along with kotlin_serialization providing us with the object serialization functionality for our kotlin code. We then add the required dependencies for the networking side of things via the use of ktor. For ktor we need the core package, along with the json and serialization parts so that we can also handle those things within the requests we make and the responses we receive using ktor.

We then need to go ahead and do the same for our android networking dependencies:

sourceSets["androidMain"].dependencies {
    implementation(Deps.Kotlin.serializationRuntime)
    implementation(Deps.Ktor.clientAndroid)
    implementation(Deps.Ktor.clientJsonJvm)
    implementation(Deps.Ktor.clientSerializationJvm)
    implementation(Deps.Ktor.clientOkhttp)
}

These don’t differ too much from the requirements of our common dependencies. The main difference here being the use of android / jvm specific ktor dependencies – these are required for when the networking code is being compiled for the androidMain source. The only addition is is the ktor_client_okhttp dependency – this is required to provide a networking engine based on OkHttp for Android specific usage.

And finally, we’re going to do exactly the same but this time for our iOS source set, iosMain. Here we’ll add the iOS specific dependencies for our ktor and serialization dependencies.

sourceSets["iosMain"].dependencies {
    implementation(Deps.Kotlin.serializationRuntimeNative)
    implementation(Deps.Kotlin.serializationRuntimeNative)
    implementation(Deps.Ktor.clientIos)
    implementation(Deps.Ktor.clientSerializationNative)     
    implementation(Deps.Ktor.clientJsonNative)
    implementation(Deps.Ktor.clientSerializationNativeX64)
}

Based on the dependencies above, we’re going to be using ktor to handle the network requests within our module. For these requests we’re going to be using an HttpClient instance – this HttpClient class comes from the ktor library and will be used to make requests to the APIs we are communicating with. When it comes to making these requests, an HttpClientEngine is used to execute the actual requests – this engine is created when the HttpClient is instantiated. The HttpClientEngine is an important part of the HttpClient – whilst we don’t have to pass an engine in during instantiation, one is created based on the available dependencies. So because we are targeting both Android and iOS, we have added the dependencies for the native clients to our project (ktor_client_android and ktor_client_ios from our dependencies). Because these have been added, the native engine for use will be detected and used.

With that in mind, we can go ahead and configure our HttpClient knowing that for each native platform, things will be configured respectively. For this we’re going to create a class which can be used to construct an HttpClient instance (source code) – this can then be used in production code to provide the required HttpClient to our firebase authentication class.

class HttpClientProvider {
    val httpClient = HttpClient {
        install(JsonFeature) {
            serializer = KotlinxSerializer()
        }
    }
}

Here we begin by constructing a new instance of an HttpClient, using the HttpClientConfig block to configure the client with some additional requirements. Within this block we use the install() function to add a feature to our client – this feature, the JsonFeature, will allow us to serialize our API responses as JSON custom objects using the provided serializer.

For this serializer we provide a reference to the KotlinxSerializer class – this is a kotlin specific JsonSerializer instance. Using this we’ll be able to mark data classes as @Serializable, allowing us to serlialize/deserialize them for our requests and responses.


When it comes to communicating with the Firebase Auth Rest API, we’re going to be hitting two different endpoints – one for signing up and one for signing in. When communicating with these endpoints, the request that is made and the response that is returned take the same representation. We’ll begin by creating the model representation of this response (source code):

@Serializable
data class FirebaseAuthenticationResponse(
    val kind: String? = null,
    val idToken: String? = null,
    val email: String? = null,
    val refreshToken: String? = null,
    val expiresIn: String? = null,
    val localId: String? = null,
    val code: Int? = null,
    val message: String? = null
)

We mark all these fields here as nullable because depending on the state of the result, only certain fields will be populated. For example, if there is an error during the request then idToken will not be populated and vice versa. We also mark this model as Serializable – that way when we get the json response back from our request, we can parse that response into the format of this model.

Now that we have our response model in place, we can go ahead and start implementing these API calls. We’ll begin here by creating a new class that will house all of our API operations, we’ll call this FirebaseAuthenticationStore (source code):

class FirebaseAuthenticationStore(
    private val httpClient: HttpClient
) {
    ....
}

Here we need to provide an HttpClient reference when constructing an instance of our class. This may look different depending on the setup of your application, but as an example this could look something like so:

FirebaseAuthenticationStore(
    HttpClientProvider().httpClient
)

Note: If you make this an object to represent a singleton, you will currently see issues when trying to run the code on iOS platforms.

At this point we have our FirebaseAuthenticationStore created, so we’ll need to add some of the code which will handle the requests that we want to make. For this part of the process we’ll start by adding some constants for both the base URL for the API and the different endpoints that we’re going to be making requests to:

companion object {
    private const val ENDPOINT_SIGN_UP = "signUp"
    private const val ENDPOINT_SIGN_IN = "signInWithPassword"
    private const val BASE_URL = "https://identitytoolkit.googleapis.com/v1/accounts:"
}

As previously mentioned, the response from these endpoints is exactly the same – this is also the case for the requests that we make to these endpoints. For this reason, we’re going to create some shared logic within a private function which will handle the creation of the request used by these calls (source code):

private suspend fun handleAuthenticationRequest(
    apiKey: String,
    endpoint: String,
    email: String,
    password: String,
    returnSecureToken: Boolean
): FirebaseAuthenticationResponse {
    return try {
        val response = httpClient.post<HttpStatement> {
            url("$BASE_URL$endpoint")
            parameter("key", apiKey)
            parameter("email", email)
            parameter("password", password)
            parameter("returnSecureToken", returnSecureToken)
        }.execute()
        Json.parse(FirebaseAuthenticationResponse.serializer(), response.readText())
    } catch (cause: Throwable) {
        FirebaseAuthenticationResponse(
            message = cause.message
        )
    }
}

There’s a little bit going on here, so let’s first take a look at the request that is being built/executed using our httpClient:

  • We begin by defining a post request using our httpClient reference, declaring that this operation is built in the form of a HttpStatement instance. It’s important to note that at this point our request object is only being built, not executed
  • Next we use the request builder url function to declare the URL that we are going to be making a request to.
  • Following the URL, we add all of the parameters that we want to send with the request. When we use the parameter function we are appending each parameter onto the collection of parameters that we wish to send with the request
  • Finally, we call execute() on our request. When this is called, our request statement is compiled and executed. Once completed, the result of this request is downloaded in the form of an HttpResponse reference that we can use to retrieve any required details from

Now that we have built and executed our request, we need to do something with the result. Having our response in this HttpResponse format isn’t ideal for the client-side of our application, we’re going to want to parse that into a more understandable format. For this we’re going to use the serialization functionality of the FirebaseAuthenticationResponse model that we defined earlier in this article. You may recall we added a dependency for kotlin serialization during the setup of our module – well here we’re going to use the Json class and it’s parse method to take our JSON response from the request and map it to our desired model representation. Because our model is annotated using the @Serializable annotation we can access its serializer using the serializer() method. We will then pass this, along with text from our network response (accessed via the responses readText() method) to this parse method.

At this point our serialized response will be returned from the call in the form of our FirebaseAuthenticationResponseModel. Now, if something throws an exception during the request, you may notice that our call is surrounded in a try / catch – when this occurs we still return an instance of this model, just accounting for an error case within its body.


With the above defined we have the logic to make network requests, the only thing left to do is to define two different entry points for our sign-up/sign-in functionality. For this, we’re going to create two methods within our FirebaseAuthenticationStore class:

suspend fun signUp(
    apiKey: String,
    email: String,
    password: String,
    returnSecureToken: Boolean
) = handleAuthenticationRequest(apiKey, ENDPOINT_SIGN_UP, email, password, 
        returnSecureToken)

suspend fun signIn(
    apiKey: String,
    email: String,
    password: String,
    returnSecureToken: Boolean
) = handleAuthenticationRequest(apiKey, ENDPOINT_SIGN_IN, email, password, 
        returnSecureToken)

Here, each of these methods takes the same three parameters:

  • apiKey – The API key belonging to our Firebase project
  • email – The email address that the user is authenticating with
  • password – The password that the user is authenticating with
  • returnSecureToken – Whether or not both an ID and refresh token should be returned with the response

For the body of these methods we call the handleAuthenticationRequest that we previously defined above. The only difference for each of these calls is the endpoint which we pass in – you’ll notice above that for the signUp method we pass in ENDPOINT_SIGN_UP and for signIn we provide ENDPOINT_SIGN_IN. With these in place, we can call either of these methods when wanting to sign-up/sign-in and the shared logic in our handleAuthenticationRequest method will be used to handle the authentication request.

Finally, because these calls are executing asynchronous work, we mark them as suspend functions so that the calling points need to handle them correctly.


Throughout this blog post we’ve built our first Kotlin Multiplatform module which will be used to handle the networking layer of our authentication feature. Whilst only a single module for a specific function, we’ve used the ktor networking library to make networking requests, serialized the responses from these requests to our Kotlin model representation and packaged these operations inside of the corresponding classes / methods. All of these come together to provide the required authentication networking that our application required.

When it comes to testing, currently things look a little different to how we might be used to writing tests in our Kotlin modules. We’ll cover how to write tests for this code in the next article in this series. In the meantime, if you have any thoughts or questions on the above concepts then please do reach out!

About the author

hitherejoe

1 Comment

  • Just small thing (and possibly not needed in this example) but I’ve ended up constructing `KotlinxSerializer` like following to make more robust in terms of handling say added json elements that code isn’t setup to read (used to use `KotlinxSerializer(Json.nonstrict)` but that was deprecated in recent version).

    serializer = KotlinxSerializer(Json(JsonConfiguration(isLenient = true, ignoreUnknownKeys = true)))

By hitherejoe