Skip to main content

Kotlin Authentication

Authentication is a crucial aspect of backend development that ensures only authorized users can access certain resources in your application. In this guide, we'll explore how to implement secure authentication in Kotlin backend applications.

Introduction to Authentication

Authentication is the process of verifying the identity of users or systems trying to access your application. It answers the question: "Who are you?" This is different from authorization, which determines what an authenticated user is allowed to do.

In modern backend applications, several authentication methods are commonly used:

  • Session-based authentication
  • Token-based authentication (JWT)
  • OAuth 2.0
  • Basic authentication

Let's dive into implementing these authentication methods in Kotlin backend applications.

Setting Up Your Kotlin Backend Project

Before implementing authentication, you need a Kotlin backend project. We'll use Ktor, a lightweight framework for building asynchronous servers and clients in Kotlin.

First, create a new Gradle project with Kotlin DSL. Your build.gradle.kts should include:

kotlin
plugins {
kotlin("jvm") version "1.8.21"
id("io.ktor.plugin") version "2.3.0"
}

repositories {
mavenCentral()
}

dependencies {
implementation("io.ktor:ktor-server-core:2.3.0")
implementation("io.ktor:ktor-server-netty:2.3.0")
implementation("io.ktor:ktor-server-auth:2.3.0")
implementation("io.ktor:ktor-server-auth-jwt:2.3.0")
implementation("ch.qos.logback:logback-classic:1.4.7")
}

Session-Based Authentication in Kotlin

Session-based authentication is a traditional approach where the server creates a session for the user after successful login and stores a session ID in a cookie.

Implementation with Ktor

First, add the sessions dependency:

kotlin
implementation("io.ktor:ktor-server-sessions:2.3.0")

Then configure sessions in your application:

kotlin
fun Application.configureSecurity() {
install(Sessions) {
cookie<UserSession>("USER_SESSION") {
cookie.path = "/"
cookie.maxAgeInSeconds = 3600
cookie.secure = true
cookie.httpOnly = true
}
}

install(Authentication) {
session<UserSession>("auth-session") {
validate { session ->
if (session.username.isNotEmpty()) {
session
} else {
null
}
}
challenge {
call.respondRedirect("/login")
}
}
}
}

data class UserSession(val username: String)

Now implement routes for login and protected resources:

kotlin
fun Application.configureRouting() {
routing {
get("/login") {
call.respondText("Login Page", ContentType.Text.Html)
}

post("/login") {
val formParameters = call.receiveParameters()
val username = formParameters["username"] ?: ""
val password = formParameters["password"] ?: ""

// Check credentials (in a real app, verify against database)
if (isValidCredentials(username, password)) {
call.sessions.set(UserSession(username))
call.respondRedirect("/dashboard")
} else {
call.respondText("Invalid credentials", status = HttpStatusCode.Unauthorized)
}
}

authenticate("auth-session") {
get("/dashboard") {
val userSession = call.principal<UserSession>()
call.respondText("Hello, ${userSession?.username}! Welcome to your dashboard.")
}
}

get("/logout") {
call.sessions.clear<UserSession>()
call.respondRedirect("/login")
}
}
}

fun isValidCredentials(username: String, password: String): Boolean {
// In a real app, check against database
return username == "user" && password == "password"
}

Token-Based Authentication with JWT

JWT (JSON Web Tokens) is a popular token-based authentication method that allows you to securely transmit information between parties.

Implementation with Ktor

First, make sure you have the JWT dependency:

kotlin
implementation("io.ktor:ktor-server-auth-jwt:2.3.0")

Configure JWT authentication:

kotlin
fun Application.configureSecurity() {
val jwtSecret = environment.config.property("jwt.secret").getString()
val jwtIssuer = environment.config.property("jwt.issuer").getString()
val jwtAudience = environment.config.property("jwt.audience").getString()
val jwtRealm = environment.config.property("jwt.realm").getString()

install(Authentication) {
jwt("auth-jwt") {
realm = jwtRealm
verifier(
JWT
.require(Algorithm.HMAC256(jwtSecret))
.withAudience(jwtAudience)
.withIssuer(jwtIssuer)
.build()
)
validate { credential ->
if (credential.payload.getClaim("username").asString() != "") {
JWTPrincipal(credential.payload)
} else {
null
}
}
challenge { _, _ ->
call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
}
}
}
}

Create a function to generate JWT tokens:

kotlin
fun generateToken(user: User): String {
val jwtSecret = environment.config.property("jwt.secret").getString()
val jwtIssuer = environment.config.property("jwt.issuer").getString()
val jwtAudience = environment.config.property("jwt.audience").getString()

return JWT.create()
.withAudience(jwtAudience)
.withIssuer(jwtIssuer)
.withClaim("username", user.username)
.withClaim("userId", user.id)
.withExpiresAt(Date(System.currentTimeMillis() + 60000 * 60 * 24)) // 24 hours
.sign(Algorithm.HMAC256(jwtSecret))
}

data class User(val id: Int, val username: String, val password: String)

Now implement routes for JWT authentication:

kotlin
fun Application.configureRouting() {
routing {
post("/login") {
val user = call.receive<LoginRequest>()

// In a real app, verify against database
val userFound = findUser(user.username, user.password)

if (userFound != null) {
val token = generateToken(userFound)
call.respond(mapOf("token" to token))
} else {
call.respond(HttpStatusCode.Unauthorized, "Invalid credentials")
}
}

authenticate("auth-jwt") {
get("/protected") {
val principal = call.principal<JWTPrincipal>()
val username = principal!!.payload.getClaim("username").asString()
val userId = principal.payload.getClaim("userId").asInt()
call.respondText("Hello, $username with ID: $userId! This is protected content.")
}
}
}
}

data class LoginRequest(val username: String, val password: String)

fun findUser(username: String, password: String): User? {
// In a real app, check against database
return if (username == "user" && password == "password") {
User(1, "user", "password")
} else {
null
}
}

OAuth 2.0 Authentication

OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts on an HTTP service. Let's implement OAuth 2.0 with Google as the provider.

Implementation with Ktor

Add the OAuth dependency:

kotlin
implementation("io.ktor:ktor-server-auth:2.3.0")

Configure OAuth authentication:

kotlin
fun Application.configureSecurity() {
val googleClientId = environment.config.property("oauth.googleClientId").getString()
val googleClientSecret = environment.config.property("oauth.googleClientSecret").getString()

install(Authentication) {
oauth("auth-oauth-google") {
urlProvider = { "http://localhost:8080/callback" }
providerLookup = {
OAuthServerSettings.OAuth2ServerSettings(
name = "google",
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
accessTokenUrl = "https://accounts.google.com/o/oauth2/token",
requestMethod = HttpMethod.Post,
clientId = googleClientId,
clientSecret = googleClientSecret,
defaultScopes = listOf("email", "profile")
)
}
client = HttpClient(Apache)
}
}
}

Now implement the routes:

kotlin
fun Application.configureRouting() {
routing {
get("/login") {
call.respondText("Login Page", ContentType.Text.Html)
}

authenticate("auth-oauth-google") {
get("/login-google") {
// Redirects to Google's login page
}

get("/callback") {
val principal: OAuthAccessTokenResponse.OAuth2? = call.principal()
principal?.let {
val accessToken = it.accessToken

// Use the accessToken to get user info from Google
val userInfo = getUserInfo(accessToken)

// Create a session for the user
call.sessions.set(UserSession(userInfo.email))
call.respondRedirect("/dashboard")
} ?: call.respondRedirect("/login")
}
}

get("/dashboard") {
val userSession = call.sessions.get<UserSession>()
if (userSession != null) {
call.respondText("Hello, ${userSession.username}! Welcome to your dashboard.")
} else {
call.respondRedirect("/login")
}
}
}
}

data class GoogleUserInfo(val email: String, val name: String)

suspend fun getUserInfo(accessToken: String): GoogleUserInfo {
val client = HttpClient(Apache) {
install(JsonFeature) {
serializer = GsonSerializer()
}
}

val userInfo = client.get<GoogleUserInfo>("https://www.googleapis.com/oauth2/v2/userinfo") {
headers {
append(HttpHeaders.Authorization, "Bearer $accessToken")
}
}

client.close()
return userInfo
}

Basic Authentication

Basic authentication is a simple authentication scheme built into the HTTP protocol. The client sends HTTP requests with the Authorization header that contains the word Basic followed by a space and a base64-encoded username:password string.

Implementation with Ktor

Configure basic authentication:

kotlin
fun Application.configureSecurity() {
install(Authentication) {
basic("auth-basic") {
realm = "Access to the '/' path"
validate { credentials ->
if (credentials.name == "admin" && credentials.password == "admin123") {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}
}

Implement protected routes:

kotlin
fun Application.configureRouting() {
routing {
authenticate("auth-basic") {
get("/admin") {
val principal = call.principal<UserIdPrincipal>()
call.respondText("Hello, ${principal?.name}! This is admin area.")
}
}
}
}

Real-World Example: User Registration and Authentication API

Let's create a complete example of a user registration and authentication API combining JWT authentication with a simple in-memory user database.

First, let's define our user model and database:

kotlin
data class User(
val id: Int,
val email: String,
val username: String,
val passwordHash: String
)

// In-memory database (in a real app, use a real database)
object UserDatabase {
private val users = mutableListOf<User>()
private var idCounter = 1

fun addUser(email: String, username: String, passwordHash: String): User {
val user = User(idCounter++, email, username, passwordHash)
users.add(user)
return user
}

fun findUserByEmail(email: String): User? = users.find { it.email == email }

fun findUserById(id: Int): User? = users.find { it.id == id }
}

Now, let's implement password hashing:

kotlin
object PasswordHasher {
fun hashPassword(password: String): String {
return BCrypt.hashpw(password, BCrypt.gensalt())
}

fun checkPassword(password: String, hash: String): Boolean {
return BCrypt.checkpw(password, hash)
}
}

Implement the JWT token service:

kotlin
object JwtService {
private val jwtSecret = "mySecret123" // Use environment variables in a real app
private val jwtIssuer = "kotlin-auth-example"
private val jwtAudience = "users"
private val validityInMs = 3600000 * 24 // 24 hours

fun generateToken(user: User): String {
return JWT.create()
.withAudience(jwtAudience)
.withIssuer(jwtIssuer)
.withClaim("userId", user.id)
.withClaim("email", user.email)
.withClaim("username", user.username)
.withExpiresAt(Date(System.currentTimeMillis() + validityInMs))
.sign(Algorithm.HMAC256(jwtSecret))
}

fun verifier(): JWTVerifier {
return JWT
.require(Algorithm.HMAC256(jwtSecret))
.withAudience(jwtAudience)
.withIssuer(jwtIssuer)
.build()
}
}

Now, let's configure the authentication:

kotlin
fun Application.configureSecurity() {
install(Authentication) {
jwt("auth-jwt") {
realm = "Kotlin Auth Example"
verifier(JwtService.verifier())
validate { credential ->
if (credential.payload.getClaim("userId").asInt() != 0) {
JWTPrincipal(credential.payload)
} else {
null
}
}
challenge { _, _ ->
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Token is not valid or has expired"))
}
}
}
}

Finally, implement the API routes:

kotlin
fun Application.configureRouting() {
install(ContentNegotiation) {
gson {
setPrettyPrinting()
}
}

routing {
post("/register") {
val registration = call.receive<RegistrationRequest>()

// Check if user already exists
if (UserDatabase.findUserByEmail(registration.email) != null) {
call.respond(
HttpStatusCode.Conflict,
mapOf("error" to "A user with this email already exists")
)
return@post
}

// Create new user
val hashedPassword = PasswordHasher.hashPassword(registration.password)
val user = UserDatabase.addUser(
registration.email,
registration.username,
hashedPassword
)

call.respond(
HttpStatusCode.Created,
mapOf(
"message" to "User created successfully",
"user" to mapOf(
"id" to user.id,
"email" to user.email,
"username" to user.username
)
)
)
}

post("/login") {
val login = call.receive<LoginRequest>()

// Find user
val user = UserDatabase.findUserByEmail(login.email)
if (user == null || !PasswordHasher.checkPassword(login.password, user.passwordHash)) {
call.respond(
HttpStatusCode.Unauthorized,
mapOf("error" to "Invalid credentials")
)
return@post
}

// Generate token
val token = JwtService.generateToken(user)

call.respond(
mapOf(
"token" to token,
"user" to mapOf(
"id" to user.id,
"email" to user.email,
"username" to user.username
)
)
)
}

authenticate("auth-jwt") {
get("/me") {
val principal = call.principal<JWTPrincipal>()
val userId = principal!!.payload.getClaim("userId").asInt()
val user = UserDatabase.findUserById(userId)

if (user != null) {
call.respond(
mapOf(
"user" to mapOf(
"id" to user.id,
"email" to user.email,
"username" to user.username
)
)
)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "User not found"))
}
}
}
}
}

data class RegistrationRequest(val email: String, val username: String, val password: String)
data class LoginRequest(val email: String, val password: String)

Security Best Practices

When implementing authentication in Kotlin, keep these best practices in mind:

  1. Never store plain passwords - Always hash passwords using strong algorithms like BCrypt or Argon2
  2. Use HTTPS - Always serve your API over HTTPS to prevent man-in-the-middle attacks
  3. Set secure cookie flags - For session-based auth, set secure and httpOnly flags on cookies
  4. Implement proper error handling - Don't leak sensitive information in error messages
  5. Rate limit authentication attempts - Prevent brute force attacks by limiting login attempts
  6. Use short-lived tokens - Set reasonable expiration times for JWTs
  7. Validate all input - Sanitize and validate all user inputs to prevent injection attacks
  8. Keep dependencies updated - Regularly update your libraries to patch security vulnerabilities

Summary

In this guide, we've covered various methods of implementing authentication in Kotlin backend applications:

  • Session-based authentication for managing user sessions with cookies
  • JWT authentication for stateless, token-based authentication
  • OAuth 2.0 for integrating with third-party authentication providers
  • Basic authentication for simple HTTP-based authentication

We've also implemented a complete user registration and authentication API using JWT tokens and demonstrated important security practices.

Authentication is a critical component of backend applications, and understanding these concepts will help you build secure, reliable systems for your users.

Additional Resources

Exercises

  1. Extend the user registration API to include email verification
  2. Implement refresh tokens alongside access tokens in the JWT authentication example
  3. Add password reset functionality to the user authentication system
  4. Create a role-based authorization system on top of the JWT authentication
  5. Implement multi-factor authentication using SMS or authenticator apps


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)