Ktor 3.6.0 Help

Form-based authentication in Ktor Server

Form-based authentication uses a web form to collect credential information and authenticate a user. To create a web form in Ktor, you can use HTML DSL or choose between JVM template engines, such as FreeMarker, Velocity, and so on.

Add dependencies

To enable form authentication, you need to include the ktor-server-auth artifact in the build script:

implementation("io.ktor:ktor-server-auth:$ktor_version")
implementation "io.ktor:ktor-server-auth:$ktor_version"
<dependency> <groupId>io.ktor</groupId> <artifactId>ktor-server-auth-jvm</artifactId> <version>${ktor_version}</version> </dependency>

Form-based authentication flow

The form-based authentication flow might look as follows:

  1. An unauthenticated client makes a request to a specific route in a server application.

  2. A server returns an HTML page that consists at least from an HTML-based web form, which prompts a user for a username and password.

  3. When a user submits a username and password, a client makes a request containing web form data (which includes the username and password) to a server.

    POST http://localhost:8080/login Content-Type: application/x-www-form-urlencoded username=jetbrains&password=foobar

    In Ktor, you need to specify parameter names used to fetch a username and password.

  4. A server validates credentials sent by a client and responds with the requested content.

Install form authentication

To install the form authentication provider, call the form function inside the install block:

import io.ktor.server.application.* import io.ktor.server.auth.* // ... install(Authentication) { form { // Configure form authentication } }

You can optionally specify a provider name that can be used to authenticate a specified route.

Configure form authentication

Step 1: Configure a form provider

The form authentication provider exposes its settings via the FormAuthenticationProvider.Config class. In the example below, the following settings are specified:

  • The userParamName and passwordParamName properties specify parameter names used to fetch a username and password.

  • The validate function validates a username and password. The validate function checks UserPasswordCredential and returns a UserIdPrincipal in the case of successful authentication or null if authentication fails.

  • The challenge function specifies an action performed if authentication fails. For instance, you can redirect back to a login page or send UnauthorizedResponse.

install(Authentication) { form("auth-form") { userParamName = "username" passwordParamName = "password" validate { credentials -> val isValid = credentials.name == "jetbrains" && credentials.password == "foobar" if (isValid) { UserIdPrincipal(credentials.name) } else { null } } challenge { val message = "Credentials are not valid" val status = HttpStatusCode.Unauthorized call.respond(status, message) } } }

The form() function creates a scheme for a principal type of your choice. There is no install(Authentication) step: the scheme is a value you pass to the routes that need it.

data class User(val name: String) val formAuth = form<User>("auth-form") { usernameField = "username" passwordField = "password" validate { credentials -> val isValid = credentials.name == "jetbrains" && credentials.password == "foobar" if (isValid) User(credentials.name) else null } onUnauthorized = { val message = "Credentials are not valid" call.respond(HttpStatusCode.Unauthorized, message) } }

Two names differ from the classic provider. The form fields are usernameField and passwordField rather than userParamName and passwordParamName. The failure handler is onUnauthorized rather than challenge. For the full API, see Type-safe authentication.

Step 2: Protect specific resources

After configuring the form provider, you need to define a post route where the data gets sent. Then, add this route inside the authenticate function. In the case of successful authentication, you can retrieve an authenticated UserIdPrincipal inside a route handler using the call.principal function and get a name of an authenticated user.

routing { authenticate("auth-form") { post("/login") { val user = call.principal<UserIdPrincipal>() call.respondText("Hello, ${user?.name}!") } } }

Define the post route where the form data is sent, and wrap it in authenticateWith(). Inside the block, call.principal is your principal type and is never null:

routing { authenticateWith(formAuth) { post("/login") { call.respondText("Hello, ${call.principal.name}!") } } }

You can use Session authentication to store a logged-in user's ID. For example, when a user logs in using a web form for the first time, you can save a username to a cookie session and authorize this user on subsequent requests using the session provider.

14 September 2026