Practical Kotlin Multiplatform: Creating the Firebase Auth Remote Store with Ktor

This post is part two of a series adapted from Practical KMP, my book on building production Kotlin Multiplatform apps for Android and iOS. If you missed it, part one covered setting up the shared networking module.

Introduction

With our shared module now set up, we have the foundations that we need to start building out the features of our app. It’s time to move onto the next step and build the authentication layer that will get users into our app.

In this part we’ll build the complete authentication remote store, defining a repository that handles every operation our app needs for managing user accounts. We’ll be using Ktor as our networking library and Firebase Authentication’s REST API as our backend, giving us a way to sign users up, sign them in, and manage their accounts from our shared code.

Because we’re modelling the API ourselves rather than relying on a third-party SDK, our authentication layer stays portable across both Android and iOS without any platform-specific dependencies. Each operation such as signing up, signing in, refreshing tokens and updating account details is defined behind a single interface and covered by tests that validate it against a mocked client.

By the end of this part, we’ll have a fully functional authentication remote store, ready to be consumed by the use cases and view models we’ll build on top of it.

Creating the Authentication Remote Store

We’re going to start here by creating the class that will be used to handle the networking for the authentication flow. While authentication will be handled through the Firebase REST API, we’ll be creating an abstraction here so that our app will not need to know any of these details. We can think of this as an internal library located inside of our project, for which we’ll create a new interface that will be used to define the supported authentication operations.

interface FirebaseAuthenticationRepository {

}

Before we start adding operations to this interface, we’ll create a new class, AuthenticationRemote, that will implement this interface and the operations that are defined within its contract.

class AuthenticationRemote() : FirebaseAuthenticationRepository {

}

For this class to be able to execute HTTP requests, we’ll need to have access to an HttpClient instance which is used to make the required network requests. So that we can also allow this class to be testable, we’ll want this to be provided through the constructor of the class. This way, we can use a default instance for when this class is provided through our application, along with a mocked instance when this class is being used within our tests.

class AuthenticationRemote(
    client: HttpClientEngine = HttpClient().engine
) : FirebaseAuthenticationRepository {
    
  constructor() : this(HttpClient().engine)

}

With this in place we now have a class that will be used to house our authentication functions, configured in a way that allows us to abstract this information from our app while also being written in a way that keeps the class testable.

Setting up Ktor

Before we can implement our authentication endpoints, we need to configure our existing AuthenticationRemote class so that we can handle all our network communication with the Firebase Authentication API. This class will serve as the implementation of our FirebaseAuthenticationRepository interface and will be responsible for managing network requests and responses.

We’ll start by creating the basic class structure within the implementation of our repository.

class AuthenticationRemote(
    client: HttpClientEngine = HttpClient().engine
) : FirebaseAuthenticationRepository {
    
constructor() : this(HttpClient().engine)

    private val client = HttpClient(client)

}

This initial setup uses a constructor approach that allows dependency injection of a custom HTTP client engine. The class implements our FirebaseAuthenticationRepository interface, ensuring it will implement all of the authentication operations that are defined in our contract.

Next, we need to configure our HTTP client with the plugins that will enable Ktor to handle JSON serialisation and deserialisation. As our authentication API responses will be in JSON format, we’ll need to install the ContentNegotiation plugin with JSON support.

class AuthenticationRemote(
    client: HttpClientEngine = HttpClient().engine
) : FirebaseAuthenticationRepository {
    
  constructor() : this(HttpClient().engine)
    
  private val client = HttpClient(client) {
    install(ContentNegotiation) {
      json(
        Json { ignoreUnknownKeys = true }
      )
    }
  }
}

The ContentNegotiation plugin with this JSON configuration will automatically handle the conversion between our Kotlin data classes and JSON format. This means that our data objects will be automatically serialised to JSON, and for any received responses, the JSON will be automatically deserialised into our response models.

Finally, we’ll add the base URL constants that we’ll use throughout our authentication implementation. These constants define the endpoints for different Firebase Authentication operations.

class AuthenticationRemote(
    client: HttpClientEngine = HttpClient().engine
) : FirebaseAuthenticationRepository {
    
  constructor() : this(HttpClient().engine)
    
  private val client = HttpClient(client) {
        install(ContentNegotiation) {
            json(
                Json { ignoreUnknownKeys = true }
            )
        }
    }

    private val authenticationBase =
        "https://identitytoolkit.googleapis.com/v1/accounts"
    private val refreshBase =
        "https://securetoken.googleapis.com/v1/token"

}

Here the authenticationBase URL will be for primary authentication operations such as sign-up and sign-in, while the refreshBase URL is there to allow us to support token refresh operations for authenticated users.

With this foundation in place, our AuthenticationRemote class is now ready for the implementation of our authentication endpoints. The configured HTTP client will handle all the networking details, while our base URLs provide the targets for any API requests.

Defining the models

Before we implement those endpoints, we need to model the data they’ll return. When the user completes the sign-up process for our app, the API will return us with a collection of data that we need to manage in our application. In this part we’ll model the response that we’re going to get back from the API.

Modelling the success state

When it comes to the authentication process in our app, the user will complete the sign-up flow and we will receive back the information regarding the result of that process. Upon a successful result, we will get back information that will allow us to:

  • Identify the authenticated user
  • Make API requests accessing their data
  • Refresh expired tokens

So that we can model this information, we’ll start by creating a data class that represents the authentication response from our service:

@Serializable
data class AuthenticationResult(

)

Here we’ve used the @Serializable annotation, which comes from the Kotlin Serialisation library and provides a way to convert between Kotlin objects and various data formats like JSON. The API response we get back is in the JSON format, so this annotation will allow that response to be converted to this data model.

Now that we have this class defined, we’re ready to start modelling the data inside of it. To begin with we’ll focus on the success response.

@Serializable
data class AuthenticationResult(
    val idToken: String? = null,
    val email: String? = null,
    val refreshToken: String? = null,
    val expiresIn: String? = null
)

Here we’ve defined each of these fields as nullable, which is because we are communicating with a REST API which may also return a failure state. In these cases, all of these fields would be null and we provide defaults here so that in these error states, the default values are automatically populated.

Now that we have these properties defined, let’s take a look at what each of these is used for:

  • idToken — a Firebase Auth ID token that has been assigned to the newly created user
  • email — the email address for the newly created user. This would be the email that they provided during sign-up
  • refreshToken — the Firebase Authentication refresh token for the newly created user. This will be used when making requests to refresh the access token
  • expiresIn — the number of seconds until the ID token expires

With this in place, we now have a model that represents the structure of a successful authentication response from a sign-up request.

Modelling the failure state

Alongside the success state, the request to authenticate could also fail. With this in mind, we also need to model this failure state into the result model. When the error state comes back from this API request, it comes in the form of a message that is nested inside of an error object. To model this, we’ll start by creating a new data class, Error.

@Serializable
data class Error(
    val message: String
)

This class follows the same principles as the data class we previously created, using the @Serializable annotation so that the response can be parsed from its JSON format.

With this Error class in place we can now slot this into our AuthenticationResult class using the error name.

@Serializable
data class AuthenticationResult(
    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 error: Error? = null
)

Now when the server is unable to successfully complete the authentication request, we will get back the error cause in the API response. At this point we won’t worry too much about what this message may represent, as we’ll tackle this in an upcoming part when we need to implement the handling of these errors.

With the store configured and its response modelled, our AuthenticationRemote class is now ready for the implementation of our authentication endpoints.


This post is adapted from Practical KMP, my book on building real Kotlin Multiplatform apps for Android and iOS. If you’d like the full journey of building apps with Kotlin Multiplatform grab a copy.