Connect Ktor with Firebase

Glitch
7 min readMar 30, 2021

--

Firebase helps you build and run successful apps. Backed by Google, loved by developers. Accelerate app development with fully managed backend infrastructure. Learn more today. Customize Your App. Cross-Platform Solutions. Boost App Engagement.

With the emergence of Ktor, it’s now possible to write a real independent

backend in Kotlin, which is widely popular among Android developers. See Backend for mobile engineers with Kotlin and Ktor for more details.

Firebase Authentication provides backend services, easy-to-use SDKs, and ready-made UI libraries to authenticate users to your app. It supports authentication using passwords, phone numbers, popular federated identity providers like Google, Facebook and Twitter, and more.

Authentication is one of the most common features in any application, hence any backend. It’s essential to know who is doing what and restrict access to users’ data and actions.

Official Ktor documentation says:

Ktor supports authentication out of the box as a standard pluggable feature. It supports mechanisms to read credentials, and to authenticate principals.

Where the is something that can be authenticated: a user, a computer, a group, and credentials can be user/password combination, token, etc.

Once you add the dependency for ktor-auth in your build.gradle, you need to install Authentication feature directly to the application, and configure it.

This is an example from the Codeforces WatchR project’s backend, which tries to replicate the famous “Bearer token” authentication with a unique token generated and mapped for every user.

Then we wrap routes, which need authentication with the authenticate(auth) call (in our case user() endpoints). It's also needed to implement a separate set of routes for Sign In / Sign Up to generate bearer tokens ( auth()).

To get the token in authenticated calls use call.request.headers["token"].

As a mobile engineer, I had never thought about how complex the authentication flow can be from the backend side. It was taken for granted because we always had endpoints at our disposition.

But when we started to write backend ourselves, we confronted with so many complexities, including, but not limited to:

  • secure and reliable tokens generation, which isn’t as simple as UUID.randomUUID()But may require a complicated setup and business-logic (see JWT and JWK documentation)
  • tokens rotation and refresh, so if they get exposed, hackers won’t get life-long access to user accounts
  • 3rd party authentication using OAuth to be able to login with Google, Facebook, Apple, Twitter, GitHub, and so on
  • admin capabilities for managing authenticated users, resetting passwords, blocking fraudsters, etc.

Fortunately, Firebase uses JWT authentication under the hood and gives access to bearer tokens from both admin and client SDKs. It means that you can delegate the whole authentication flow to Firebase and use generated tokens to authenticate users on your backend.

First up we’re going to create a new Ktor project using the IntelliJ IDEA Ktor plugin. We’ll choose which features we’d like to use, and we’ll tell the plugin that we’d like to use the Kotlin Gradle DSL. You can choose how you’d like to deal with content negotiation, I’m using GSON. For the HTTPClient Engine I’m using Jetty, and of course we’ll also need to tick Authentication. I’ve also ticked call logging so I can see what endpoints I’ve called.

Once you’re done choosing features follow the prompts until you have an initialised project. To complete the Ktor setup replace the current build.gradle.kts file with the one below and load the changes.

Now let’s setup our Firebase Project and generate a new private key for the Firebase Admin setup. The Firebase docs are pretty good, so if you’re not sure how to create a Firebase project, head over here to find out. Once you have a new project, navigate to the project settings, then to service accounts. There you can click the generate new private key button.

This will download a json file which you then place in the resources directory of your project, and REMEMBER if you plan to push to a public repo, add it to your .gitignore file!

Great, we’re setup and can start looking at the code :)

First up, let’s add a few more Kotlin files. From your root directory, create the file auth/firebase/FirebaseAuth.kt, then create object files config/firebase/AuthConfig and config/firebase/FirebaseAdmin, and lastly create data class file model/User. Once you’re done your folder structure should look like the image below.

This article assumes basic knowledge of the Ktor framework — if you’d like to learn more about the basics though, there are loads of tutorials covering those topics online! I used quite a few of them when I was getting started recently.

Let’s start with the Firebase Admin code. In the FirebaseAdmin file paste the below code (remember to change the name of the json file to the one you downloaded),

/**
* Initialization for Firebase application.
*/
object FirebaseAdmin {
private val serviceAccount: InputStream? =
this::class.java.classLoader.getResourceAsStream("the-name-of-your-firebase-adminsdk.json")
private val options: FirebaseOptions = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build()
fun init(): FirebaseApp = FirebaseApp.initializeApp(options)
}

Then add the lineFirebaseAdmin.init() above the feature install code in your Application.kt file. This will use the credentials you downloaded to initialise your Firebase app.

Next up add the following code to your FirebaseAuth.kt file,

private val firebaseAuthLogger: Logger = LoggerFactory.getLogger("io.robothouse.auth.firebase")class FirebaseAuthenticationProvider internal constructor(config: Configuration) : AuthenticationProvider(config) {    internal val token: (ApplicationCall) -> String? = config.token
internal val principle: ((uid: String) -> Principal?)? = config.principal
class Configuration internal constructor(name: String?) : AuthenticationProvider.Configuration(name) { internal var token: (ApplicationCall) -> String? = { call -> call.request.parseAuthorizationToken() } internal var principal: ((uid: String) -> Principal?)? = null internal fun build() = FirebaseAuthenticationProvider(this)
}
}
fun Authentication.Configuration.firebase(
name: String? = null,
configure: FirebaseAuthenticationProvider.Configuration.() -> Unit
) {
val provider = FirebaseAuthenticationProvider.Configuration(name).apply(configure).build()
provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
try {
val token = provider.token(call) ?: throw FirebaseAuthException(
FirebaseException(
ErrorCode.UNAUTHENTICATED,
"No token could be found",
null
)
)
val uid = FirebaseAuth.getInstance().verifyIdToken(token).uid provider.principle?.let { it.invoke(uid)?.let { principle -> context.principal(principle) } } } catch (cause: Throwable) {
val message = if (cause is FirebaseAuthException) {
"Authentication failed: ${cause.message ?: cause.javaClass.simpleName}"
} else {
cause.message ?: cause.javaClass.simpleName
}
firebaseAuthLogger.trace(message)
call.respond(HttpStatusCode.Unauthorized, message)
context.challenge.complete()
finish()
}
}
register(provider)
}
fun ApplicationRequest.parseAuthorizationToken(): String? = authorization()?.let {
it.split(" ")[1]
}

The above code supplies an AuthenticationProvider which is then built with the nested Configuration class. This Configuration class provides access to the token and a lambda function (which we will set up later) to supply the Principle (being the User).

Next we have the Authentication.Configuration.firebase extension function which will be doing all the actual work for us. Here we build the AuthenticationProvider, we then use it to setup an interceptor; inside this interceptor, we fetch the token and check that the token is valid. Lastly we invoke the lambda to fetch our Principle, and of course, we do this within a try/catch so that we can catch and handle any errors that might occur.

Now let’s move onto the AuthConfig and User files. The AuthConfig will supply the lambda function to our Configuration so that we can fetch our Principle and the the User file will be our data model.

data class User(
val _id: String,
val username: String
): Principal

Notice that our User data class above implements the Principle interface. This is because our Configuration is expecting that type. Once you’ve added the User data class code, add the below code to your AuthConfig file.

/**
* Configuration for [FirebaseAuthenticationProvider].
*/
object AuthConfig {
fun FirebaseAuthenticationProvider.Configuration.configure() {
principal = { uid ->
//this is where you'd make a db call to fetch your User's profile data
runBlocking { User(uid, "myUsername") }
}
}
}

The above code supplies an extension function for our Configuration class, and as I mentioned earlier, this then supplies the lambda to fetch our Principle. Lastly we’ll edit our Application.kt file to use all the code we’ve added.

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)@Suppress("unused") // Referenced in application.conf
fun Application.module() {
// initialize Firebase Admin SDK
FirebaseAdmin.init()
install(ContentNegotiation) { gson { setPrettyPrinting() } }
install(Authentication) { firebase { configure() } }
install(CallLogging) {
level = Level.INFO
filter { call -> call.request.path().startsWith("/") }
}
routing {
get("/") {
call.respond(HttpStatusCode.OK, "I'm working just fine, thanks!")
}
authenticate {
get("/authenticated") {
call.respond(HttpStatusCode.OK, "My name is ${call.principal<User>()?.username}, and I'm authenticated!")
}
}
}
}

In the above code you can see that when the Authentication feature is installed we’ve used our firebase extension function and handed it our configure extension function. Now when we make any calls to authenticated endpoints, our application will look for a token in the request headers, if found the token will be validated and finally our User data will be added to the call and can be retrieved by calling call.principle<User>().

And with all of this, we can now signup/login with Firebase on our frontend and get a bearer token to use when making secure calls to our backend. We can let Firebase take care of things like refreshing the token, sending email address validation emails and updating a User’s email or password.

Ta-da!! that’s complete our firebase and Ktor part .Keep coding.

--

--

No responses yet