I'm building a web app with Ktor in Kotlin and just added the authentication with form-auth and session-auth. Now the check if the user is logged in only happens on the /home route. I have several routes like /books, /changelog and so on. So how can I configure my Routing.kt to check before every route if the is user is logged in and if not to show the login form?
After the check I want to display the session name in a Freemarker template in my UI. At the moment the name is only shown on the /home route. On the other routes I only get the placeholder "Hello Guest". I'm passing the variable to my Freemarker _layout file.
How can I pass the name of the session globally to Freemarker to see it on every route?
Here is my code so far:
Routing.kt:
fun Application.configureRouting() {
routing {
static("/static") {
resources("files")
}
get("/") {
call.respondRedirect("home")
}
route("login") {
get {
call.respondHtml {
head {
style {
unsafe {
+"""
.container {
height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
}
form {
text-align: center;
}
"""
}
}
}
body {
div("container") {
form(
action = "/login",
encType = FormEncType.applicationXWwwFormUrlEncoded,
method = FormMethod.post
) {
p {
+"Username:"
textInput(name = "username")
}
p {
+"Password:"
passwordInput(name = "password")
}
p {
submitInput() { value = "Login" }
}
}
}
}
}
}
}
authenticate("auth-form") {
post("/login") {
val userName = call.principal<UserIdPrincipal>()?.name.toString()
call.sessions.set(UserSession(name = userName))
call.respondRedirect("/home")
}
}
authenticate("auth-session") {
get("/home") {
val userSession = call.principal<UserSession>()
val name = userSession?.name
call.respond(FreeMarkerContent("home.ftl", mapOf("user" to mapOf("name" to name))))
}
}
get("/logout") {
call.sessions.clear<UserSession>()
call.respondRedirect("/login")
}
route("changelog") {
get {
call.respond(FreeMarkerContent("changelog.ftl", model = null))
}
}
route("books") {
get {
call.respond(FreeMarkerContent("books.ftl", model = null))
}
}
}
}
Application.kt:
fun Application.module() {
install(Sessions) {
cookie<UserSession>("user_session") {
cookie.path = "/"
cookie.maxAgeInSeconds = 3600
}
}
install(Authentication) {
form("auth-form") {
userParamName = "username"
passwordParamName = "password"
validate { credentials ->
if (credentials.name == Constants.userName && credentials.password == Constants.password) {
UserIdPrincipal(credentials.name)
} else {
null
}
}
challenge {
call.respond(HttpStatusCode.Unauthorized, "Credentials are not valid")
}
}
session<UserSession>("auth-session") {
validate { session ->
if (session.name.startsWith("jet")) {
session
} else {
null
}
}
challenge {
call.respondRedirect("/login")
}
}
}
DatabaseFactory.init()
// configureSerialization()
configureTemplating()
configureRouting()
}
_layout.ftl:
<#macro header>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="/static/style.css" />
<link href="/static/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<title>Ktor App.</title>
</head>
<body style="text-align: center; font-family: sans-serif" onload="loadImage()">
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-dark bg-primary border-bottom shadow">
<div class="container">
<a class="navbar-brand"> <img src="/static/favicon-16x16.png" /> Books</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="#navbarNav"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse" id="navbarNav">
<ul class="navbar-nav" style="display: flex; align-items: center;">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/books">Books</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/changelog">Changelog</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/search">Search</a>
</li>
<#if user?? && user.name?has_content>
<li class="nav-item">
<a class="nav-link"> <i class="fa fa-user" aria-hidden="true" style="color:white;"></i> ${user.name}</a>
<#else>
<a class="nav-link"> <i class="fa fa-user" aria-hidden="true" style="color:white;"></i> Hello Guest</a>
</#if>
</li>
<li class="nav-item">
<a class="btn btn-danger" href="/logout">
<i class="fa fa-sign-out" aria-hidden="true"></i>
Logout
</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<#--<img src="/static/ktor_logo.png">
<h1>Kotlin Ktor Journal </h1>
<p><i>Powered by Ktor & Freemarker!</i></p>
<hr>-->
<#nested>
<!-- <a href="/">Back to the main page</a> -->
<footer class="border-top footer bg-teal text-light text-center fixed-bottom">
<div class="container">
© 2022
</div>
</footer>
<script src="/static/switch.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" crossorigin="anonymous"></script>
</body>
</html>
</#macro>
You can write a helper function to merge a specific model with a session data:
suspend fun ApplicationCall.respondFreemarker(template: String, model: Map<Any, Any>) {
val session = sessions.get<UserSession>()
respond(FreeMarkerContent(template, mapOf("session" to session) + model))
}
Here is a full example:
fun main() {
embeddedServer(Netty, host = "0.0.0.0", port = 12345) {
install(Sessions) {
cookie<UserSession>("cookie_name")
}
install(FreeMarker) {
templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates")
}
install(Authentication) {
session<UserSession>("auth-session") {
validate { session ->
session
}
challenge {
call.respond(HttpStatusCode.Unauthorized)
}
}
}
routing {
authenticate("auth-session") {
get("/a") {
call.respondFreemarker("index.ftl", mapOf())
}
get("/b") {
call.respondFreemarker("index.ftl", mapOf())
}
}
get("/login") {
val name = call.request.queryParameters["name"] ?: return@get
call.sessions.set(UserSession(name))
call.respond(HttpStatusCode.OK)
}
}
}.start(wait = true)
}
suspend fun ApplicationCall.respondFreemarker(template: String, model: Map<Any, Any>) {
val session = sessions.get<UserSession>()
respond(FreeMarkerContent(template, mapOf("session" to session) + model))
}
data class UserSession(val name: String): Principal
index.ftl:
<html>
<body>
<h1>${session.name}</h1>
</body>
</html>