Authentication and authorization plugin for Ktor server applications
—
Form-based authentication for HTML forms and session-based authentication for maintaining user sessions. These providers are commonly used together in web applications for traditional login/logout workflows.
HTML form-based authentication that processes username and password from POST form data.
fun AuthenticationConfig.form(
name: String? = null,
configure: FormAuthenticationProvider.Config.() -> Unit
)
@KtorDsl
class FormAuthenticationProvider.Config(name: String?) : AuthenticationProvider.Config(name) {
var userParamName: String
var passwordParamName: String
fun validate(body: suspend ApplicationCall.(UserPasswordCredential) -> Any?)
fun challenge(body: FormAuthChallengeFunction)
fun challenge(redirectUrl: String)
fun challenge(redirect: Url)
fun skipWhen(predicate: ApplicationCallPredicate)
}userParamName: HTML form parameter name for username (default: "user")passwordParamName: HTML form parameter name for password (default: "password")validate: Suspend function that validates form credentialschallenge: Function to handle authentication failures (typically redirects to login page)challenge(redirectUrl: String): Convenience function to redirect to URL on failurechallenge(redirect: Url): Convenience function to redirect to Url on failureskipWhen: Predicate to skip authentication for certain callsinstall(Authentication) {
form("auth-form") {
userParamName = "username"
passwordParamName = "password"
validate { credentials ->
if (credentials.name == "admin" && credentials.password == "secret") {
UserIdPrincipal(credentials.name)
} else {
null
}
}
challenge {
call.respondRedirect("/login?error=true")
}
}
}
routing {
authenticate("auth-form") {
post("/secure-form") {
val principal = call.principal<UserIdPrincipal>()
call.respond("Form submitted by ${principal?.name}")
}
}
}Session-based authentication that maintains user sessions using Ktor's session management.
fun <T : Any> AuthenticationConfig.session(
name: String? = null,
configure: SessionAuthenticationProvider.Config<T>.() -> Unit
)
@KtorDsl
class SessionAuthenticationProvider.Config<T : Any>(name: String?) : AuthenticationProvider.Config(name) {
fun validate(body: suspend ApplicationCall.(T) -> Any?)
fun challenge(body: SessionAuthChallengeFunction<T>)
fun challenge(redirectUrl: String)
fun challenge(redirect: Url)
fun skipWhen(predicate: ApplicationCallPredicate)
}validate: Function that validates session data and returns principalchallenge: Function to handle session authentication failureschallenge(redirectUrl: String): Convenience function to redirect to URL on failurechallenge(redirect: Url): Convenience function to redirect to Url on failureskipWhen: Predicate to skip authentication for certain calls// Define session data class
@Serializable
data class UserSession(val userId: String, val loginTime: Long)
install(Sessions) {
cookie<UserSession>("user_session") {
cookie.httpOnly = true
cookie.secure = true
}
}
install(Authentication) {
session<UserSession>("auth-session") {
validate { session ->
// Validate session is not expired
if (Clock.System.now().epochSeconds - session.loginTime < 3600) {
UserIdPrincipal(session.userId)
} else {
null
}
}
challenge {
call.respondRedirect("/login")
}
}
}
routing {
authenticate("auth-session") {
get("/profile") {
val principal = call.principal<UserIdPrincipal>()
call.respond("Profile for ${principal?.name}")
}
}
}Typical web application pattern combining form login with session maintenance:
@Serializable
data class UserSession(val userId: String, val loginTime: Long)
install(Sessions) {
cookie<UserSession>("user_session") {
cookie.httpOnly = true
cookie.secure = true
cookie.maxAgeInSeconds = 3600
}
}
install(Authentication) {
// Form authentication for login
form("login-form") {
userParamName = "username"
passwordParamName = "password"
validate { credentials ->
userService.authenticate(credentials)
}
challenge {
call.respondRedirect("/login?error=invalid")
}
}
// Session authentication for protected pages
session<UserSession>("user-session") {
validate { session ->
userService.getUser(session.userId)?.let { user ->
UserIdPrincipal(user.id)
}
}
challenge {
call.respondRedirect("/login")
}
}
}
routing {
// Login page and form processing
get("/login") {
call.respondText(loginPageHtml, ContentType.Text.Html)
}
authenticate("login-form") {
post("/login") {
val principal = call.principal<UserIdPrincipal>()!!
call.sessions.set(UserSession(principal.name, Clock.System.now().epochSeconds))
call.respondRedirect("/dashboard")
}
}
// Protected pages using session auth
authenticate("user-session") {
get("/dashboard") {
val principal = call.principal<UserIdPrincipal>()
call.respond("Welcome to dashboard, ${principal?.name}!")
}
post("/logout") {
call.sessions.clear<UserSession>()
call.respondRedirect("/")
}
}
}fun <T : Any> AuthenticationConfig.session(name: String? = null): SessionAuthenticationProvider<T>
fun <T : Any> AuthenticationConfig.session(
name: String?,
kClass: KClass<T>
): SessionAuthenticationProvider<T>
fun <T : Any> AuthenticationConfig.session(
name: String? = null,
configure: SessionAuthenticationProvider.Config<T>.() -> Unit
): SessionAuthenticationProvider<T>
fun <T : Any> AuthenticationConfig.session(
name: String?,
kClass: KClass<T>,
configure: SessionAuthenticationProvider.Config<T>.() -> Unit
): SessionAuthenticationProvider<T>class FormAuthenticationProvider internal constructor(
config: Config
) : AuthenticationProvider(config)
typealias FormAuthChallengeFunction = suspend PipelineContext<*, ApplicationCall>.(
context: FormAuthChallengeContext
) -> Unit
data class FormAuthChallengeContext(val call: ApplicationCall)class SessionAuthenticationProvider<T : Any> internal constructor(
config: Config<T>
) : AuthenticationProvider(config)
typealias SessionAuthChallengeFunction<T> = suspend PipelineContext<*, ApplicationCall>.(
context: SessionChallengeContext
) -> Unit
data class SessionChallengeContext(val call: ApplicationCall)
val SessionAuthChallengeKey: Stringform("multi-step-auth") {
validate { credentials ->
val step = call.parameters["step"]
when (step) {
"1" -> {
// First step: validate username
if (userService.userExists(credentials.name)) {
// Store partial authentication state
call.sessions.set(PartialAuth(credentials.name))
null // Don't complete authentication yet
} else null
}
"2" -> {
// Second step: validate password + 2FA
val partial = call.sessions.get<PartialAuth>()
if (partial?.username == credentials.name) {
val twoFactorCode = call.parameters["2fa_code"]
if (userService.validateCredentials(credentials) &&
twoFactorService.verify(credentials.name, twoFactorCode)) {
call.sessions.clear<PartialAuth>()
UserIdPrincipal(credentials.name)
} else null
} else null
}
else -> null
}
}
}@Serializable
data class UserSession(val userId: String, val roles: List<String>)
session<UserSession>("admin-session") {
validate { session ->
if ("admin" in session.roles) {
UserIdPrincipal(session.userId)
} else null
}
challenge {
call.respond(HttpStatusCode.Forbidden, "Admin access required")
}
}session<UserSession>("database-session") {
validate { session ->
// Validate against database instead of just session data
transaction {
Sessions.select { Sessions.sessionId eq session.sessionId }
.singleOrNull()
?.let { row ->
if (row[Sessions.expiresAt] > Clock.System.now()) {
UserIdPrincipal(row[Sessions.userId])
} else null
}
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-ktor--ktor-server-auth-jvm