Ktor server for beginners – Login & Authentication – Part 1

Level: Beginner

In the previous lesson we learned how to store files and provide them as static content, at the moment anyone with access to our server’s address can add or delete fruits and while this might sound convenient we want to know who added what so we know who to blame when we see wrong entries :). lets implement a register and login functionality to our server!

Preparing

To register users we will need to receive their information when a client makes a request, lets make a data class that will represent our account request in the data > model package

@Serializable
data class AccountRequest(
    val username: String,
    val password: String
)

lets create another data class to represent our user, while in this example the AccountReqeust and the User are smililar in a real project they are likely not the same as we don’t usually require the user to add all his information when he registers.

@Serializable
data class User(
    @BsonId
    val username: String,
    val password: String
)

Registering Users

Lets implement the basic functionality to add a new user to our database, let’s add this to our Database.kt file in the data > db package, the logic here is very similar to what we covered in the previous articles so im not going to explain them in depth as the name of the functions suggest the purpose of them.

suspend fun addUser(user: User): Boolean {
    return try {
        users.insertOne(user).wasAcknowledged()
    } catch (ex: Exception) {
        ex.printStackTrace()
        false
    }
}

suspend fun checkIfUserExists(user: User): Boolean {
    return users.findOne(User::username eq user.username) != null
}

In the routes package create a new kotlin file and call it UserRoutes, in this file we will define our login and register routes, lets start with the register route

fun Route.userRoute(){
    post("/register"){
        val user = try {
            call.receive<AccountRequest>()
        } catch (ex: ContentTransformationException) {
            call.respond(HttpStatusCode.BadRequest, SimpleResponse(success = false, message = "Invalid body"))
            return@post
        }
        // check if username already exists first
        if (checkIfUserExists(user.username)){
            // username already exists
            call.respond(HttpStatusCode.OK, SimpleResponse(success = false, message = "Username already exists"))
        }else{
            // username does not exist add user to db
            if (addUser(User(user.username, user.password))){
                // successfully added
                call.respond(HttpStatusCode.OK, SimpleResponse(success = true, message = "Successfully added user"))
            } else {
                // some error happened
                call.respond(HttpStatusCode.OK, SimpleResponse(success = false, message = "Unknown Error"))
            }
        }
    }
}

Login

Add the following function to check if the user exists and compare the sent password the with one saved in the database.

suspend fun checkPasswordForEmail(username: String, passwordToCheck: String): Boolean {
    // return false if we can't find the username in our db
    val actualPassword = users.findOne(User::username eq username)?.password ?: return false
    return actualPassword == passwordToCheck
}

add the login route in UserRoutes

    post("/login"){
        val user = try {
            call.receive<AccountRequest>()
        } catch (ex: ContentTransformationException) {
            call.respond(HttpStatusCode.BadRequest, SimpleResponse(success = false, message = "Invalid body"))
            return@post
        }
        val isPasswordCorrect = checkUsernameForPassword(user.username,user.password)
        if (isPasswordCorrect) {
            call.respond(HttpStatusCode.OK, SimpleResponse(success = true, message = "Login successful"))
        } else {
            call.respond(HttpStatusCode.OK, SimpleResponse(success = false, message = "Incorrect credentials"))
        }
    }

Try out your new routes! register and login and checkout the users collection in mongodb

Securely saving passwords

If you notice our database in Mongo compass right now passwords are saved in our database in plain text, this is very insecure if for some reason our database got leaked this means our users passwords will be exposed also the site admin / database adminstrator will be able to see user passwords. but how do we hide our password while still saving it in our databse and compare it when the user logs in to make sure he entered the correct password?

1. Hash the password

When we hash a password a hashing algorithm transforms your password into a fixed-length string of characters, known as a hash.

Imagine you have a secret code that you want to keep safe. You can use a special machine called a “password hasher.” When you input your secret code into the password hasher, it performs a series of calculations and transformations to produce a unique output, which is the hashed version of your secret code.

The key point here is that the password hasher always produces the same hash for the same input, but it’s computationally infeasible to reverse-engineer the original secret code from the hash. So, even if someone gains access to the hashed password, they cannot easily determine what the original password was.

2. Salt the password

When hashing a password it gets transformed to a unique string generated from that password if two users have the same password they will end up having the same hashed password in the database so to prevent someone from guessing passwords by comparing them in addition to hashing we’re going to add salt to our passwords.

Salt is a random string added to the password before we apply hashing function to it.

Add the commons codec dependency to your build.gradle file

implementation("commons-codec:commons-codec:1.15")

In the root package of our project create a new package and call it security in it create a new Kotlin file let’s call it PasswordUtil this file will hold our hash and salt functions

the getHashWithSalt function takes our password and generates a salt, hashes the password then return the salt appended to the hashed password, notice that to generate the salt we use SecureRandom and not the regular Random function because that one can be predictable easily and SecureRandom is more secure, in the second step we convert the salt to a hexadecimal string because the salt we get from generateSeed is a byte array.

checkHashForPassword basically takes the password sent by client hashes it using the same salt and compares the hash results to the ones saved in the database.

import org.apache.commons.codec.binary.Hex
import org.apache.commons.codec.digest.DigestUtils
import java.security.SecureRandom

fun getHashWithSalt(stringToHash: String, saltLength: Int = 32): String {
    val salt = SecureRandom.getInstance("SHA1PRNG").generateSeed(saltLength)
    val saltAsHex = Hex.encodeHexString(salt)
    val hash = DigestUtils.sha256Hex("$saltAsHex$stringToHash")
    return "$saltAsHex:$hash"
}

fun checkHashForPassword(password: String, hashWithSalt: String): Boolean {
    val hashAndSalt = hashWithSalt.split(":")
    val salt = hashAndSalt[0]
    val hash = hashAndSalt[1]
    val passwordHash = DigestUtils.sha256Hex("$salt$password")
    return hash == passwordHash
}

lets use these functions in our register and login routes, for the login we only need to change the checkUsernameForPassword function in the database

val user = try {
            call.receive<AccountRequest>()
        } catch (ex: ContentTransformationException) {
            call.respond(HttpStatusCode.BadRequest, SimpleResponse(success = false, message = "Invalid body"))
            return@post
        }
        // check if username already exists first
        if (checkIfUserExists(user.username)){
            // username already exists
            call.respond(HttpStatusCode.OK, SimpleResponse(success = false, message = "Username already exists"))
        }else{
            // username does not exist hash the password
            val hashedPassword = getHashWithSalt(user.password)
            // add user to db
            if (addUser(User(user.username, hashedPassword))){
                // successfully added
                call.respond(HttpStatusCode.OK, SimpleResponse(success = true, message = "Successfully added user"))
            } else {
                // some error happened
                call.respond(HttpStatusCode.OK, SimpleResponse(success = false, message = "Unknown Error"))
            }
        }
suspend fun checkUsernameForPassword(username: String, passwordToCheck: String): Boolean {
    // return false if we can't find the username in our db
    val actualPassword = users.findOne(User::username eq username)?.password ?: return false
    return checkHashForPassword(passwordToCheck, actualPassword)
}

now try registering a new user and view the collection in compass. its not plain text anymore!

Authenticating users

Now that we successfully register and login using our API we would like to allow access to a specific end points to registered users only, to do that we’ll need to implement some kind of authentication we’ll learn Basic and JWT but since this tutorial is getting long already let’s start with Basic Authentication and leave JWT for the next lesson.

Basic Authentication

Basic authentication is the most basic form of authentication, here’s how it works when the client makes a request it sends the username and password encoded using Base64 in the request header, this is considered not secure since it passes the username and password in clear text that’s why its not recommended to use basic authentication over normal HTTP, the better and more secure way to use basic authentication is in combination with HTTPS/TSL to encrypt the traffic going from and to the server.

Now let’s add the authentication dependency to our build.gradle.kts file

implementation("io.ktor:ktor-server-auth:$ktor_version")

in the plugins package create a new Kotlin file “Security” in this file we will setup our basic authentication plugin

in the validate block we need to provide a way for the authentication plugin to verify if the username and password are correct, the UserIdPricipal function provides the username for the current logged in user so we can use it later in our routes without needing the user to send it as a parameter (we’ll see how in the next step), the realm here is just a name to the protected area that we would like to authenticate, it is mainly used for informational purposes and does not directly affect the authentication process itself.

fun Application.configureBasicAuthentication() {
    authentication {
        basic {
            realm = "Fruits Server"
            validate { credentials ->
                if (checkUsernameForPassword(credentials.name, credentials.password)) {
                    UserIdPrincipal(credentials.name)
                } else {
                    null
                }
            }
        }
    }
}

make sure to call configureBasicAuthentication in your Application.module function Before the configureRouting function to avoid any errors

now to the best part! lets prevent unregistered users from adding new fruits to our database we can simply do that by wrapping our add-fruit end point with an authenticate block

authenticate {
        post("/add-fruit") {
            try {
                
                // get the username for the authenticated user
                val username = call.principal<UserIdPrincipal>()?.name ?: throw Exception("Can't get username")
                
                // receive multipart data from the client
                val multipart = call.receiveMultipart()

                // define variable to hold our parameters data
                var fileName: String? = null
                var name: String? = null
                var season: String? = null
                val countries: MutableList<String> = mutableListOf()
                var imageUrl: String? = null

                try {
                    // loop through each part of our multipart
                    multipart.forEachPart { partData ->
                        when (partData) {
                            is PartData.FormItem -> {
                                // to read parameters that we sent with the image
                                when (partData.name) {
                                    "name" -> name = partData.value
                                    "season" -> season = partData.value
                                    "countries" -> countries.add(partData.value)
                                }
                            }

                            is PartData.FileItem -> {
                                // to read the image data we call the 'save' utility function passing our path
                                if (partData.name == "image") {
                                    fileName = partData.save(Constants.FRUIT_IMAGE_PATH)
                                    imageUrl = "${Constants.BASE_URL}${Constants.EXTERNAL_FRUIT_IMAGE_PATH}/$fileName"
                                }
                            }

                            else -> Unit
                        }
                    }
                } catch (ex: Exception) {
                    // something went wrong with the image part, delete the file
                    File("${Constants.FRUIT_IMAGE_PATH}/$fileName").delete()
                    ex.printStackTrace()
                    call.respond(HttpStatusCode.InternalServerError, "Error")
                }
                // create a new fruit object using data we collected above
                val newFruit = Fruit(
                    name = name!!,
                    // the valueOf function will find the enum class type that matches this string and return it, an Exception is thrown if the string does not match any type
                    season = Fruit.Season.valueOf(season!!),
                    countries = countries,
                    image = imageUrl,
                    addedBy = username
                )
                // add the received fruit to the database
                if (!addFruit(newFruit)) {
                    // if not added successfully return with an error
                    return@post call.respond(
                        HttpStatusCode.Conflict,
                        SimpleResponse(success = false, message = "Item already exits")
                    )
                }

                // acknowledge that we successfully added the fruit by responding
                call.respond(HttpStatusCode.Created, newFruit)
            } catch (ex: Exception) {

                call.respond(HttpStatusCode.BadRequest, SimpleResponse(false, "Invalid data"))
            }

        }
    }

Notice how we got the username of the current authenticated user using UserIdPricipal?, lets save the username with the fruit item to do that we’ll need to modify the Fruit data class by adding the addedBy field, we set the default value to Unknown so it can get assigned to items that are already in the database before we added the new field alternatively you can make it null-able and give it a default value of null

     val addedBy: String = "Unknown"

lets run our server and give it a test run on postman :D, if you try to make a add a fruit you should get an Unauthorized response, to authenticate the request we need to go the Authorization tab and select Basic Authentication from the type drop down menu

send the request and notice how your username was added as part of the object you added to the database.

Congrats! now you know who to blame when you see incorrect entries in the db :P, in part 2 we’re going to learn how to implement JWT based authentication and use a Bearer token instead instead of the basic authentication method of sending the credentials on every request.

The source code can be found here ,and last but not least we can’t end this without a cat gif

5 2 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments