Ktor: REST API for Mobile

In this tutorial, you’ll create a REST API server for mobile apps using the new Ktor framework from JetBrains. By Kevin D Moore.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Authenticating Your Users

You need to authenticate users to keep your server secure. When you created your server, you chose the JWT authentication feature. But you’ll need to create a few functions to use it.

Start by creating a new file titled Auth.kt under auth and add the following:

import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.hex
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

@KtorExperimentalAPI // 1
val hashKey = hex(System.getenv("SECRET_KEY")) // 2

@KtorExperimentalAPI
val hmacKey = SecretKeySpec(hashKey, "HmacSHA1") // 3

@KtorExperimentalAPI
fun hash(password: String): String { // 4
    val hmac = Mac.getInstance("HmacSHA1")
    hmac.init(hmacKey)
    return hex(hmac.doFinal(password.toByteArray(Charsets.UTF_8)))
}
  1. Makes use of the SECRET_KEY Environment Variable defined in step 2. Use this value as the argument of the hex function, which turns the HEX key into a ByteArray. Note the use of @KtorExperimentalAPI to avoid warnings associated with the experimental status of the hex function.
  2. Defines Environment Variable.
  3. Creates a secret key using the given algorithm, HmacSHA1.
  4. hash converts a password to a string hash.

Next, create a new class in auth named JwtService and add the following:

import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import com.raywenderlich.models.User
import java.util.*

class JwtService {

    private val issuer = "todoServer"
    private val jwtSecret = System.getenv("JWT_SECRET") // 1
    private val algorithm = Algorithm.HMAC512(jwtSecret)

    // 2
    val verifier: JWTVerifier = JWT
        .require(algorithm)
        .withIssuer(issuer)
        .build()

    // 3
    fun generateToken(user: User): String = JWT.create()
        .withSubject("Authentication")
        .withIssuer(issuer)
        .withClaim("id", user.userId)
        .withExpiresAt(expiresAt())
        .sign(algorithm)

    private fun expiresAt() = 
        Date(System.currentTimeMillis() + 3_600_000 * 24) // 24 hours
}

The previous code requires the JWT_SECRET Environment Variable in step 1 to create the JWTVerifier in step 2. generateToken, defined in step 3, generates a token that the API uses to authenticate the request. You’ll need this function later.

Next, create a file named MySession in the auth folder and add this code:

data class MySession(val userId: Int)

This stores the current userId in a Session.

Configuring Application

In this section, you’ll add the required plumbing to configure the server. To do so, you’ll use some of the functions you created earlier.

Open Application.kt, remove MySession and import the newer MySession you just created.

Now, add the following after the install(Sessions) section:

// 1
DatabaseFactory.init()
val db = TodoRepository()
// 2
val jwtService = JwtService()
val hashFunction = { s: String -> hash(s) }

Import these classes. The first part of this code initializes the data layer you defined earlier, while the second part handles authentication.

Next, add the following to the install(Authentication) section:

jwt("jwt") { //1
    verifier(jwtService.verifier) // 2
    realm = "Todo Server"
    validate { // 3
        val payload = it.payload
        val claim = payload.getClaim("id")
        val claimString = claim.asInt()
        val user = db.findUser(claimString) // 4
        user
    }
}

You define the JWT name in step 1, which can be anything you want.

Step 2 specifies the verifier you created in the JwtService class.

Step 3 creates a method that runs each time the app needs to authenticate a call.

Finally, step 4 tries to find the user in the database with the userId from claimString. If the userID exists, it verifies the user. Otherwise, it returns a null user and rejects the route.

Before you continue, run your server to make sure everything compiles and runs properly. You should see several Hikari debug statements in the output window.

Hikari debug statements

Building the Routes

If you started your own project, here’s an extra step for you. At the bottom of Application.kt, add the following constant:

const val API_VERSION = "/v1"

This is a constant that you will use to prefix all the paths in your routes.

Adding the User Create Route

In Routes, create a new file named UserRoute.

Start by adding the following imports to the top of the file:

import com.raywenderlich.API_VERSION
import com.raywenderlich.auth.JwtService
import com.raywenderlich.auth.MySession
import com.raywenderlich.repository.Repository
import io.ktor.application.application
import io.ktor.application.call
import io.ktor.application.log
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.Route
import io.ktor.sessions.sessions
import io.ktor.sessions.set

These will avoid issues of choosing the wrong version of a class. Next, add some route constants:

const val USERS = "$API_VERSION/users"
const val USER_LOGIN = "$USERS/login"
const val USER_CREATE = "$USERS/create"

These define the strings needed for the routes, allowing you to create or log in a user.

Add the following route classes:

@KtorExperimentalLocationsAPI
@Location(USER_LOGIN)
class UserLoginRoute

@KtorExperimentalLocationsAPI
@Location(USER_CREATE)
class UserCreateRoute

Since Locations API is experimental, using @KtorExperimentalLocationsAPI removes any compiler warnings. @Location(USER_LOGIN) associates the string USER_LOGIN with the UserLoginRoute class.

Then add the user create route:

@KtorExperimentalLocationsAPI
// 1
fun Route.users(
    db: Repository, 
    jwtService: JwtService, 
    hashFunction: (String) -> String
) {  
    post<UserCreateRoute> { // 2
        val signupParameters = call.receive<Parameters>() // 3
        val password = signupParameters["password"] // 4
            ?: return@post call.respond(
                    HttpStatusCode.Unauthorized, "Missing Fields")
        val displayName = signupParameters["displayName"] 
            ?: return@post call.respond(
                    HttpStatusCode.Unauthorized, "Missing Fields")
        val email = signupParameters["email"] 
            ?: return@post call.respond(
                    HttpStatusCode.Unauthorized, "Missing Fields")
        val hash = hashFunction(password) // 5
        try {
            val newUser = db.addUser(email, displayName, hash) // 6
            newUser?.userId?.let {              
                call.sessions.set(MySession(it))
                call.respondText(
                    jwtService.generateToken(newUser), 
                    status = HttpStatusCode.Created
                )
            }
        } catch (e: Throwable) {
            application.log.error("Failed to register user", e)
            call.respond(HttpStatusCode.BadRequest, "Problems creating User")
        }
    }
}

Here’s how all that breaks down.

Adding the TODO Routes

Retrieving TODOs

Where to Go From Here?

Testing Routes

Adding the User Login Route

Test the Server

Handling TODOs

Testing the Server

Test the Server

But you’re not done yet. Add the following code to the routing section in Application.kt:

Build and run the server again.

Perform a Build ▸ Build Project to ensure your project still builds without errors.

The Postman app is a terrific tool for testing APIs. Find it at getpostman.com/.

Once installed, open Postman and use a new tab to make a POST request to generate a new user. Use localhost:8080/v1/users/create. In the Body tab, add these three variables: displayName, email and password.

Use any data you want in the Value column. Press Send and you’ll get a token back.

Testing routes in Postman

To save that token in Postman, set a global variable that you can use in other calls. To do so, go to the Tests tab and add:

This sets a variable named jwt_token that you can use in other calls.

Setting up the jwt_token

Now that your server can generate jwt tokens, its time to add some routes to perform authentication.

Open UserRoutes and add the following after the UserCreateRoute:

Here’s the breakdown.

Restart your server and open Postman. Use a new tab to create a POST request to log in a user. Use localhost:8080/v1/users/login.

In the Body tab, add two variables: email and password using the data from the create call in the Value column. Press Send and you’ll get a token back.

Postman setup

Perform another Build ▸ Build Project to ensure your project builds without errors.

Before adding the TODO routes, you need to adapt the repository. Open Repository and after findUserByEmail add:

Open TodoRepository and add after rowToUser:

Here’s what the code above does:

Now, create the routes for handling TODOs.

Create a new file named TodosRoute in the routes folder and add the following code:

This defines one route for TODOs. Since you want to create, delete or retrieve TODOs, you can use one route with three different REST methods: POST, DELETE and GET.

To do so, add the following code below the previous one:

This code is similar to the code you used to create a User:

Next, open Application.kt and add the TODO route after the users route in the routing section:

Restart your server and open Postman. Since you restarted the server, you’ll have to log in again.

Create a new tab to add a new TODO. First, you’ll need to add the authorization token you got when you created the user.

Go to the Authorization tab and add the jwt_token variable in the token field:

Postman with the jwt_token in the token field

Next, go to the Body tab. Create a TODO parameter, or key. The value field is the name of the TODO.

Set the done state by adding another key named done and setting its value to true.

Give a value for the TODO and press Send. You’ll see the JSON result in the body section below.

JSON results in Postman

To cap things off, in the next section, you’ll create the routes to fetch TODOs.

Now that your users can save their TODOs, they need to be able to call them up again as well.

Open TodosRoute and add the following underneath the post TodoRoute function:

Restart your server and open Postman. Log in so you have an updated token, then create a new tab to get your TODOs.

Add the authorization token you got when you created the user and press Send. You’ll see something like:

Results in Postman

Congratulations! This was a loooong journey, but you’ve reached your destination. You’ve created a REST Server!

Download the final version of this project using the Download Materials button at the top or bottom of this tutorial.

If you want to learn more about Ktor, swing over to ktor.io/, or checkout the video course if you’re interested in learning how to dockerize and deploy your Ktor server to the cloud.

If you’re interested in learning about how to consume REST APIs from Android, checkout the video course on the site.

We hope you enjoyed this tutorial. If you have any questions, comments or musings, please join the forum discussion below.

  1. Defines an extension function to Route named users that takes in a Repository, a JWTService and a hash function.
  2. Generates a route for creating a new user.
  3. Uses the call parameter to get the parameters passed in with the request.
  4. Looks for the password parameter and returns an error if it doesn’t exist.
  5. Produces a hash string from the password.
  6. Adds a new user to the database.
  7. users(db, jwtService, hashFunction)
    
    var data = responseBody;
    postman.clearGlobalVariable("jwt_token");
    postman.setGlobalVariable("jwt_token", data);
    
        post<UserLoginRoute> { // 1
            val signinParameters = call.receive<Parameters>()
            val password = signinParameters["password"]
                    ?: return@post call.respond(
                           HttpStatusCode.Unauthorized, "Missing Fields")
            val email = signinParameters["email"] 
                    ?: return@post call.respond(
                           HttpStatusCode.Unauthorized, "Missing Fields")
            val hash = hashFunction(password)
            try {
                val currentUser = db.findUserByEmail(email) // 2
                currentUser?.userId?.let {
                    if (currentUser.passwordHash == hash) { // 3
                        call.sessions.set(MySession(it)) // 4
                        call.respondText(jwtService.generateToken(currentUser)) // 5
                    } else {
                        call.respond(
                           HttpStatusCode.BadRequest, "Problems retrieving User") // 6
                    }
                }
            } catch (e: Throwable) {
                application.log.error("Failed to register user", e)
                call.respond(HttpStatusCode.BadRequest, "Problems retrieving User")
            }
        }
    
    1. Defines a POST for logging in a user.
    2. Tries to find the user by their email address.
    3. Compares the hash created from the passed-in password with the existing hash.
    4. If the hashes don’t match, the method returns an error. Otherwise, it sets the current session for the user ID in step 4 and creates a token from the user to return in step 5.
        suspend fun addTodo(userId: Int, todo: String, done: Boolean): Todo?
        suspend fun getTodos(userId: Int): List<Todo>
    
        // 1
        override suspend fun addTodo(userId: Int, todo: String, done: Boolean): Todo? {
            var statement : InsertStatement<Number>? = null
            dbQuery {
                statement = Todos.insert {
                    it[Todos.userId] = userId
                    it[Todos.todo] = todo
                    it[Todos.done] = done
                }
            }
            return rowToTodo(statement?.resultedValues?.get(0))
        }
    
        // 2
        override suspend fun getTodos(userId: Int): List<Todo> {
            return dbQuery {
                 Todos.select {
                    Todos.userId.eq((userId)) // 3
                }.mapNotNull { rowToTodo(it) }
            }
        }
        // 4
        private fun rowToTodo(row: ResultRow?): Todo? {
            if (row == null) {
                return null
            }
            return Todo(
                id = row[Todos.id],
                userId = row[Todos.userId],
                todo = row[Todos.todo],
                done = row[Todos.done]
            )
        }
    
    1. Defines addTodo, which takes a user ID, the TODO text and the done flag.
    2. Defines the method to get all TODOs for a given user ID.
    3. Note how getTodos uses eq to find a user that matches the user ID.
    4. Defines a helper function to convert an Exposed ResultRow to your TODO class.
    import com.raywenderlich.API_VERSION
    import io.ktor.application.application
    import io.ktor.application.call
    import io.ktor.application.log
    import io.ktor.auth.authenticate
    import io.ktor.http.HttpStatusCode
    import io.ktor.http.Parameters
    import io.ktor.locations.*
    import io.ktor.request.receive
    import io.ktor.response.respond
    import io.ktor.routing.Route
    import io.ktor.sessions.get
    import io.ktor.sessions.sessions
    import com.raywenderlich.auth.MySession
    import com.raywenderlich.repository.Repository
    
    const val TODOS = "$API_VERSION/todos"
    
    @KtorExperimentalLocationsAPI
    @Location(TODOS)
    class TodoRoute
    
    @KtorExperimentalLocationsAPI
    fun Route.todos(db: Repository) {
        authenticate("jwt") { // 1
            post<TodoRoute> { // 2
                val todosParameters = call.receive<Parameters>()
                val todo = todosParameters["todo"] 
                    ?: return@post call.respond(
                         HttpStatusCode.BadRequest, "Missing Todo")
                val done = todosParameters["done"] ?: "false"
                // 3
                val user = call.sessions.get<MySession>()?.let { 
                    db.findUser(it.userId) 
                }
                if (user == null) {
                    call.respond(
                         HttpStatusCode.BadRequest, "Problems retrieving User")
                    return@post
                }
    
                try {
                    // 4
                    val currentTodo = db.addTodo(
                         user.userId, todo, done.toBoolean())
                    currentTodo?.id?.let {
                        call.respond(HttpStatusCode.OK, currentTodo)
                    }
                } catch (e: Throwable) {
                    application.log.error("Failed to add todo", e)
                    call.respond(HttpStatusCode.BadRequest, "Problems Saving Todo")
                }
            }
        }
    }
    
    1. Uses the authenticate extension function to tell the system you want to authenticate these routes.
    2. Defines the new TODO route.
    3. Checks if the user has a session. If not, it returns an error.
    4. Adds the TODO to the database.
      todos(db)
      
      get<TodoRoute> {
          val user = call.sessions.get<MySession>()?.let { db.findUser(it.userId) }
          if (user == null) {
              call.respond(HttpStatusCode.BadRequest, "Problems retrieving User")
              return@get
          }
          try {
              val todos = db.getTodos(user.userId)
              call.respond(todos)
          } catch (e: Throwable) {
              application.log.error("Failed to get Todos", e)
              call.respond(HttpStatusCode.BadRequest, "Problems getting Todos")
          }
      }
      
  1. Defines a POST for logging in a user.
  2. Tries to find the user by their email address.
  3. Compares the hash created from the passed-in password with the existing hash.
  4. If the hashes don’t match, the method returns an error. Otherwise, it sets the current session for the user ID in step 4 and creates a token from the user to return in step 5.
  1. Defines addTodo, which takes a user ID, the TODO text and the done flag.
  2. Defines the method to get all TODOs for a given user ID.
  3. Note how getTodos uses eq to find a user that matches the user ID.
  4. Defines a helper function to convert an Exposed ResultRow to your TODO class.
  1. Uses the authenticate extension function to tell the system you want to authenticate these routes.
  2. Defines the new TODO route.
  3. Checks if the user has a session. If not, it returns an error.
  4. Adds the TODO to the database.
    todos(db)
    
    get<TodoRoute> {
        val user = call.sessions.get<MySession>()?.let { db.findUser(it.userId) }
        if (user == null) {
            call.respond(HttpStatusCode.BadRequest, "Problems retrieving User")
            return@get
        }
        try {
            val todos = db.getTodos(user.userId)
            call.respond(todos)
        } catch (e: Throwable) {
            application.log.error("Failed to get Todos", e)
            call.respond(HttpStatusCode.BadRequest, "Problems getting Todos")
        }
    }
    
users(db, jwtService, hashFunction)
var data = responseBody;
postman.clearGlobalVariable("jwt_token");
postman.setGlobalVariable("jwt_token", data);
    post<UserLoginRoute> { // 1
        val signinParameters = call.receive<Parameters>()
        val password = signinParameters["password"]
                ?: return@post call.respond(
                       HttpStatusCode.Unauthorized, "Missing Fields")
        val email = signinParameters["email"] 
                ?: return@post call.respond(
                       HttpStatusCode.Unauthorized, "Missing Fields")
        val hash = hashFunction(password)
        try {
            val currentUser = db.findUserByEmail(email) // 2
            currentUser?.userId?.let {
                if (currentUser.passwordHash == hash) { // 3
                    call.sessions.set(MySession(it)) // 4
                    call.respondText(jwtService.generateToken(currentUser)) // 5
                } else {
                    call.respond(
                       HttpStatusCode.BadRequest, "Problems retrieving User") // 6
                }
            }
        } catch (e: Throwable) {
            application.log.error("Failed to register user", e)
            call.respond(HttpStatusCode.BadRequest, "Problems retrieving User")
        }
    }
    suspend fun addTodo(userId: Int, todo: String, done: Boolean): Todo?
    suspend fun getTodos(userId: Int): List<Todo>
    // 1
    override suspend fun addTodo(userId: Int, todo: String, done: Boolean): Todo? {
        var statement : InsertStatement<Number>? = null
        dbQuery {
            statement = Todos.insert {
                it[Todos.userId] = userId
                it[Todos.todo] = todo
                it[Todos.done] = done
            }
        }
        return rowToTodo(statement?.resultedValues?.get(0))
    }

    // 2
    override suspend fun getTodos(userId: Int): List<Todo> {
        return dbQuery {
             Todos.select {
                Todos.userId.eq((userId)) // 3
            }.mapNotNull { rowToTodo(it) }
        }
    }
    // 4
    private fun rowToTodo(row: ResultRow?): Todo? {
        if (row == null) {
            return null
        }
        return Todo(
            id = row[Todos.id],
            userId = row[Todos.userId],
            todo = row[Todos.todo],
            done = row[Todos.done]
        )
    }
import com.raywenderlich.API_VERSION
import io.ktor.application.application
import io.ktor.application.call
import io.ktor.application.log
import io.ktor.auth.authenticate
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.locations.*
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Route
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import com.raywenderlich.auth.MySession
import com.raywenderlich.repository.Repository

const val TODOS = "$API_VERSION/todos"

@KtorExperimentalLocationsAPI
@Location(TODOS)
class TodoRoute
@KtorExperimentalLocationsAPI
fun Route.todos(db: Repository) {
    authenticate("jwt") { // 1
        post<TodoRoute> { // 2
            val todosParameters = call.receive<Parameters>()
            val todo = todosParameters["todo"] 
                ?: return@post call.respond(
                     HttpStatusCode.BadRequest, "Missing Todo")
            val done = todosParameters["done"] ?: "false"
            // 3
            val user = call.sessions.get<MySession>()?.let { 
                db.findUser(it.userId) 
            }
            if (user == null) {
                call.respond(
                     HttpStatusCode.BadRequest, "Problems retrieving User")
                return@post
            }

            try {
                // 4
                val currentTodo = db.addTodo(
                     user.userId, todo, done.toBoolean())
                currentTodo?.id?.let {
                    call.respond(HttpStatusCode.OK, currentTodo)
                }
            } catch (e: Throwable) {
                application.log.error("Failed to add todo", e)
                call.respond(HttpStatusCode.BadRequest, "Problems Saving Todo")
            }
        }
    }
}
todos(db)
get<TodoRoute> {
    val user = call.sessions.get<MySession>()?.let { db.findUser(it.userId) }
    if (user == null) {
        call.respond(HttpStatusCode.BadRequest, "Problems retrieving User")
        return@get
    }
    try {
        val todos = db.getTodos(user.userId)
        call.respond(todos)
    } catch (e: Throwable) {
        application.log.error("Failed to get Todos", e)
        call.respond(HttpStatusCode.BadRequest, "Problems getting Todos")
    }
}