You are already familiar with the basics of routing in Ktor. What if you want to restrict access to some pages for unauthorized users? Today we'll get acquainted with two convenient authentication tools available in Ktor.
Basic authentication
Basic authentication is a mechanism built into the HTML protocol that makes it easy to limit access to the page by username and password.
Suppose we have a page (localhost:8080/page) that is protected by basic authentication. The mechanism of interaction between the client and the server, in this case, will look as follows:
First, the client makes a usual request to localhost:8080/page. The server sees the page is protected and returns information to the client indicating that authorization is required (401 Unauthorized). The response will contain a header:
WWW-Authenticate: Basic realm="Access to the '/page' path", charset="UTF-8"The WWW-Authenticate header tells the browser that the page is protected by authentication, specifying its type, encoding, and realm(text message from the server that the browser can show the user).
Upon receiving such a response, the browser will display a dialog box for the user to enter the username and password:
After the user enters his username and password, the browser will encode them using Base64 and make a second request to get the page. This time, the request will have an authentication data header:
Authorization: Basic amV0YnJhaW5zOmZvb2JhcgThe server checks the login and password that are in the Authorization header and, if successful, returns the requested page to the user.
Basic authentication in Ktor
Ktor has a handy plugin that allows you to connect Basic authentication to your Routing handlers easily.
First, you need to add the dependency:
implementation("io.ktor:ktor-server-auth:$ktor_version")implementation "io.ktor:ktor-server-auth:$ktor_version"Now we have to install the plugin. To do this, we need to call the install(Authentication) function:
import io.ktor.server.application.*
import io.ktor.server.auth.*
fun Application.module() {
install(Authentication) {
basic("myAuth") {
// Configure basic authentication
}
}
}To set the Basic authentication provider, we call the basic function inside the plugin's configuration block. The basic function accepts an optional authentication provider name, such as "myAuth". We will need this name to refer to this specific configuration later in Routing. Keep in mind that provider names must be unique, and you can define only one provider without a name which will be taken as the default provider.
Now let's configure Basic authentication:
install(Authentication) {
basic("myAuth") {
realm = "Access to the '/page' path"
validate { credentials ->
if (credentials.name == "Admin" && credentials.password == "2425") {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}The basic block exposes the realm and validate settings.
The realm property sets the realm to be passed in WWW-Authenticate header.
The validate function compares the username and password with the correct ones and returns a UserIdPrincipal(with this object we can access the name of the user who has logged in within the Routing handler) in a case of successful authentication, or null if authentication fails. Within the validate function, there can be any validation of the entered data, including database calls. For simplicity, we just compare them to "Admin" and "2425".
Now let's connect our authentication to the /page handler:
routing {
get("/page") {
call.respondText("Protected content!")
}
}To protect the page, we wrap the appropriate handler in the authenticate function and pass the authentication provider name to it (or omit the name if you have a default provider):
routing {
authenticate("myAuth") {
get("/page") {
call.respondText("Protected content!")
}
}
}Also inside the handler, we can access the name under which the user is logged in using the call.principal function:
routing {
authenticate("myAuth") {
get("/page") {
val user = call.principal<UserIdPrincipal>()
call.respondText("Protected content! Name: ${user?.name}")
}
}
}The /page can now only be accessed after entering the correct username and password:
If you entered the correct credentials, you will see the page:
After you log in successfully, the browser saves your username and password. So you don't need to enter them every time you refresh the page.
To protect multiple pages with "myAuth" authentication, you just need to place their handlers inside the authenticate("myAuth") block:
routing {
authenticate("myAuth") {
get("/page") {
call.respondText("Protected content!")
}
get("/page2") {
call.respondText("Another protected content!")
}
get("/page3") {
call.respondText("Another protected content2!")
}
}
}Form authentication
Now let's look at a more common way to protect your pages, Form authentication.
Suppose we have a page (localhost:8080/page) that is protected by Form authentication. Page localhost:8080/auth contains an HTML authorization form with login and password fields. The mechanism of interaction between the client and the server, in this case, will look:
First, the client makes a request to localhost:8080/page. The server sees the page is protected by form authentication and redirects the client to the page with the authorization form.
After filling out this form, the user sends a POST request with login and password to /page. The server checks the login and password and, if successful, returns the requested page to the user. If authorization fails, the user will be redirected back to the authorization page.
As you can see, Form authentication is very similar to Basic authentication. The advantage of form authorization is that we can change the design of the form on the /auth page. You can't change the design of the dialog box in Basic authorization.
Form authentication in Ktor
Just as with Basic authentication, Ktor has a handy Form authorization plugin.
Make sure the following dependency is included (as shown previously):
implementation("io.ktor:ktor-server-auth:$ktor_version")implementation "io.ktor:ktor-server-auth:$ktor_version"Now let's install the Form authentication plugin:
import io.ktor.server.application.*
import io.ktor.server.auth.*
fun Application.module() {
install(Authentication) {
form("myFormAuth") {
// Configure form authentication
}
}
}To set the form authentication provider, we call the form function inside the plugin's configuration block. We pass our provider name "myFormAuth" as an argument to the function (as before, this is optional).
The configuration of this plugin is:
install(Authentication) {
form("myFormAuth") {
userParamName = "username"
passwordParamName = "password"
validate { credentials ->
if (credentials.name == "Admin" && credentials.password == "2425") {
UserIdPrincipal(credentials.name)
} else {
null
}
}
challenge {
call.respondRedirect("/auth")
}
}
}Inside the form block, we have four settings: userParamName, passwordParamName, validate and challenge.
userParamName and passwordParamName contain the names of the form parameters that will be sent in the POST request.
The validate function works exactly as it did in Basic authentication: it checks the credentials and returns a UserIdPrincipal upon success or null upon failure.
Finally, the challenge function sets the action to perform if the user is not authenticated or has entered invalid credentials. In this case, we redirect them to the /auth route.
Now let's connect our authorization to the /page handler:
routing {
authenticate("myFormAuth") {
post("/page") {
val userName = call.principal<UserIdPrincipal>()?.name
call.respondText("Protected content! Name: $userName")
}
}
get("/page") {
call.respondRedirect("/auth")
}
}Note that the authenticate block wraps the POST /page route. This is where the form submits data. For users who directly access /page using the GET method, we manually redirect them to the authorization form using call.respondRedirect.
The last thing we need to do is provide the authorization form itself. We send the appropriate HTML code to the user:
get("/auth") {
val formHtml = """
<form action="/page" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="Auth">
</form>
""".trimIndent()
call.respondText(formHtml, ContentType.Text.Html)
}We set the Content-Type: text/html header so that the browser treats our response as HTML code, not just text.
Note the action and method attributes of the form. They specify that when the user clicks the button, a POST request will be sent to the /page. We also set the name attributes of the input fields (username, password) to match the ones we specified when we configured the form authentication.
Now you can only access the content of /page by correctly filling out the authentication form.
Conclusion
In this topic, we learned about implementing a simple authentication in Ktor.
We covered the mechanisms of Basic and Form authorization and their implementation in Ktor
Keep in mind that:
With Basic authentication, the user enters the data in a native dialog box provided by the browser.
With Form authentication, the user is usually redirected to a HTML page with an authorization form.
Now let's put what we've learned into practice.