In this blog we will be building a simple Authentication API with Ktor and then connecting it to a MongoDB database. This blog is aimed at developers with some experience with kotlin and Ktor.
Introduction to Ktor
First of all, what is Ktor?
Well in simple terms, Ktor is a framework used to build web applications, HTTP services, mobile and browser applications. But due to the scope of this lesson, we will be focusing on HTTP services. For a more detailed introduction to Ktor, please visit:Link
Getting started with ktor
To get Ktor on your machine, I recommend the official guide Link. Note, if you are using the IntelliJ IDEA Community Edition, navigate down to the Create a new ktor project heading and click on Ktor Project Generator. For this lesson, we will be requiring these features:
Security
Authentication JWT (JSON Web Token) &
*Sessions (Sessions holds the state of the current user)
**Routing*
Locations &
*Routing(Locations and Routing helps us to utilize the API routes)
**HTTP*
ContentNegotiation
**Serialization*
*GSON(Used to serialize and deserialize Java objects)
Authenticating with ktor
Ktor makes it very easy to create an authentication layer. As we all know, security is a vital ingredient in most applications, as it helps in the effective use of the application. There are various means of authenticating our apps but we will be using JWT today. For more info about JWT visit Link.
To utilize JWT, first create a JWTConfig.kt class and add the following code:
class JwtService {
private const val secret = "cXRIJ57575KKFDJFcmcm"
private const val issuer = "ktor.io"
private val algorithm = Algorithm.HMAC512(secret)
val verifier: JWTVerifier = JWT
.require(algorithm)
.withIssuer(issuer)
.build()
fun generateToken(userId: String): String = JWT.create()
.withSubject("Authentication")
.withIssuer(issuer)
.withClaim("userId", userId)
.withExpiresAt(expiresAt())
.sign(algorithm)
private fun expiresAt() =
Date(System.currentTimeMillis() + 3_600_000 * 24) // 24 hours
}
*The verifier value checks whether a token is correct
*The generateToken method generates the token that will be returned to the user
We now call the class in our Application.kt file and add these lines of code
val jwtService = JwtService()
fun Application.module(testing: Boolean = false) {
install(Authentication) {
jwt{
verifier(jwtService.verifier)
realm = JWT_REALM
validate {
val userId = it.payload.getClaim(CLAIM_USERID).asInt()
val user = mongoDataHandler.finduser(userId) // 4
PrincipalUser(user?.userId!!.toString())
}
}
}
data class PrincipalUser(val userId: String): Principal
*The validate method returns a validated user.
For a more detailed explanation on JWT authentication, I recommend this Youtube tutorial by Caelis : Link
Integrating with a Database
To start, we need Docker. What is Docker? In simple terms, Docker is a container system that allows you package your software in a format that can then be run on any platform that supports Docker. For more information on docker, I recommend this 1 hour tutorial by Programming with Mosh Link
Getting started with Docker
To get Docker on your machine I recommend the official guide :Link. After the installation, head on to Intellij and create a docker-compose.yml file(Docker compose file) and add the following code:
version: '3'
services:
mymongo:
image: mongo:latest
ports:
- 27017:27017
Next add this dependency
implementation "org.mongodb:mongodb-driver:3.12.9"
What is a Docker compose file? As stated in the Docker docs Link;
When you’re developing software, the ability to run an application in an isolated environment and interact with it is crucial. The Compose command line tool can be used to create the environment and interact with it.
The Compose file provides a way to document and configure all of the application’s service dependencies (databases, queues, caches, web service APIs, etc). Using the Compose command line tool you can create and start one or more containers for each dependency with a single command (docker-compose up).
Together, these features provide a convenient way for developers to get started on a project. Compose can reduce a multi-page “developer getting started guide” to a single machine readable Compose file and a few commands.
The code adds and downloads an image of the MongoDB and exposes its ports.
Next create a MongoDBdata.kt class and add the following code
class MongoDBdata{
val database: MongoDatabase
val userCollection: MongoCollection<User>
init{
val pojoCodecRegistry: CodecRegistry = fromProviders(PojoCodecProvider.builder().automatic(true).build())
val codecRegistry: CodecRegistry = fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
pojoCodecRegistry)
val clientSettings = MongoClientSettings.builder()
.codecRegistry(codecRegistry)
.build()
val mongoClient = MongoClients.create(clientSettings)
database = mongoClient.getDatabase("mydatabase");
userCollection = database.getCollection(User::class.java.name, User::class.java)
userCollection.insertOne(User(userId = null, email= "good", userName = "username", passwordHash = "password"))
}
fun adduser(email: String, username: String, password: String): User? {
userCollection.insertOne(User(userId = null, email = email, userName = username, passwordHash = password))
return userCollection.find(Filters.eq("email", email)).first()
}
fun finduser(id: Int): User?{
return userCollection.find(Filters.eq("_id", id)).first()
}
fun finduserByEmail(email: String): User?{
return userCollection.find(Filters.eq("email",email)).first()
}
}
class User(userId: ObjectId?,
email: String= "email",
userName: String = "username",
passwordHash: String= "password"): Serializable{
@BsonId
var userId: ObjectId?
var email: String
var userName: String
var passwordHash: String
constructor() : this(null, "void", "void"){}
init{
this.userId = userId
this.email = email
this.userName = userName
this.passwordHash = passwordHash
}
}
In this code, a mongoClient is created with the clientSetting, we then go on to create a database named mydatabase with the mongoClient and finally we create a userCollection passing in a User class.
The user class has an @Bson annotation which tells the database to utilize that parameter as the ID. We initialized a fake user to populate the database on creation.
The adduser method adds a user to the database and returns the user that was added.
The finduserByEmail method finds and returns an instance of the user using an email address.
The finduser method finds a user by id and returns an instance of the user.
Using Authenticated routes
First, we create a kotlin file called Routes. Here we create an extension function and utilize the Location feature. We add the following code
const val USERLOGIN = "/login"
const val USERCREATE = "/create"
val mongoDBdata= MongoDBdata()
@Location(USERCREATE)
class Register{
}
@Location(USERLOGIN)
class Login{
}
fun Routing.userRoutes(){
post<Register>{
val signupParameters = call.receiveParameters()
val password = signupParameters["password"]
?: return@post call.respond(
HttpStatusCode.Unauthorized, "Missing Fields")
val userName = signupParameters["userName"]
?: return@post call.respond(
HttpStatusCode.Unauthorized, "Missing Fields")
val email = signupParameters["email"]
?: return@post call.respond(
HttpStatusCode.Unauthorized, "Missing Fields")
try {
val user = mongoDBdata.adduser(email, userName, password)
user?.userId?.let{
call.respondText(
jwtService.generateToken(user.userId!!.toString()),
status = HttpStatusCode.Created
)
}
}catch (e: Throwable) {
application.log.error("Failed to register user", e)
call.respond(HttpStatusCode.BadRequest, "Problems creating User")
}
}
post<Login> {
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")
try {
val presentUser = mongoDBdata.finduserByEmail(email)
presentUser?.userId?.let { call.respondText(jwtService.generateToken(presentUser.userId!!.toString()))
if (currentUser.passwordHash == hash) {
call.sessions.set(MySession(it.toString()))
call.respondText(jwtService.generateToken(currentUser.userId!!.toString()))
} else {
call.respond(
HttpStatusCode.BadRequest, "Problems retrieving User")
}
}
} catch (e: Throwable) {
application.log.error("Failed to register user", e)
call.respond(HttpStatusCode.BadRequest, "Problems retrieving User")
}
}
}
data class MySession(val userId: String)
The Register route receives the parameter from the post request and calls the adduser function defined earlier to create a user and return the user created. We then extract the userId from the returned instance and utilize it in the generateToken method.
The Login route calls the finduserbyEmail function using the email received from the request and returns a token if the password matches the one entered by the current user.
Call userRoutes() in Application.kt and wrap the authenticate function around any route that needs authentication
fun Application.module(testing: Boolean = false) {
routing {
userRoutes()
authenticate {
get("/"){
call.respondText("HELLO WORLD!", contentType = ContentType.Text.Plain)
}
}
}
}
To start it up, open the docker-compose.yml in the terminal and run “docker-compose up -d”. You just successfully ran the compose file.
Next, run your server and give it a while to startup. Go back to the terminal and type enter in docker exec -it scripts_mymongo_1 sh(scripts_mymongo_1 represents the container name). To check for the name of your container enter “docker ps”.
Now enter in mongo. You are now in the mongo shell, ready to interact with mongoDB.
To show the available database enter show db.
To use a database enter use mydatabase(development represents the database name).
To show collections enter show collections.
To see contents of a collection enter db.”collection name.find(), you should see a single fake user detail.
Thank you for sticking this long, hope you learned a thing or two. See you in the next one.
Top comments (0)