Conquer Authentication with Ktor: Part 2 – Basic Authentication
Welcome to the latest chapter in our ongoing series about implementing authentication with Ktor. Having laid a solid foundation by covering the fundamentals of Ktor, we’re now prepared to delve into the realm of security protocols. In today’s post, we’re going to start with the simplest one – Basic Authentication. We’ll unpack how this protocol functions and guide you through its implementation using Ktor.
Remember, this is a hands-on tutorial. Feel free to check out the project and code along with me.
Table of Contents
- What is Basic Authentication?
- Enable Basic Authentication in Ktor
- Store User Credentials
- Configure Security
- Protect Routes with Basic Authentication
- Run It!
- Inspect Network Traffic
- Summary
What is Basic Authentication?
Basic Authentication is a simple and straightforward method for an HTTP user agent, a browser, to provide a username and password when making a request.
Here’s a step-by-step of how Basic Authentication works.
- Client sends a HTTP request: The client sends a request to the server without headers related to authentication.
- Server responds: If authentication is required, the server responds with a
401 Unauthorized
status code and aWWW-Authenticate
header. The header looks like this:WWW-Authenticate: Basic realm="User Visible Realm"
. - Client asks User for credentials: The client (browser) displays a pop-up asking the user for their username and password.
- Client authenticates: The client then must provide a username and password to the server. These credentials are concatenated with a colon (username:password), then base64 encoded, and sent to the server with an
Authorization
header. The second request from the client to the server would look like this:Authorization: Basic base64encode(username:password)
. - Authentication completes: If the credentials are correct, the server processes the request and sends a response to the client. If the credentials are incorrect, the server responds again with a
401 Unauthorized
status code, and the client is prompted to provide valid credentials.
Please note that Basic Authentication transmits the username and password in an easily reversible format. Therefore, it’s critical that you only use Basic Authentication over HTTPS to encrypt your credentials during transit. Because of the simplicity and lack of advanced features (like token refresh, multi-factor authentication), Basic Authentication is typically suitable for straightforward use cases or for internal application communication.
Enable Basic Authentication in Ktor
There’s an authentication add-on in Ktor. You can choose the specific means of authentication or opt for an authentication
bundle that contains everything. For the purpose of this tutorial, we’ll use the latter.
Ktor offers specialized authentication add-ons. You can either select a specific authentication method based on your needs or opt for the comprehensive authentication bundle that encompasses all available methods. For the purpose of this tutorial, we’ll go with the latter, providing us with the broadest scope of authentication methods to work with.
In the project’s build file (build.gradle.kts), add the following dependency.
implementation("io.ktor:ktor-server-auth-jvm")
In order to protect access to all the endpoints in our web application, we need to install the authentication add-on. Here is a simple setup taken from the documentation.
install(Authentication) {
basic("auth-basic") {
realm = "Access to the '/' path"
validate { credentials ->
if (credentials.name == "jetbrains" && credentials.password == "foobar") {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}
Are we done? Not at all! Who would want to hard-code credentials anyway? Let me show you a better approach.
Store User Credentials
For the sake of this tutorial, let’s assume that user credentials are stored as plain text in a file. However, this is not a recommended practice in a production environment due to security concerns. In a real-world scenario, we should implement additional layers of security by hashing or encrypting stored values. For now, we will omit these complexities to keep our example straightforward. Nevertheless, it is not difficult to imagine how to enhance security measures on various types of resources, such as a text file or a relational database, in order to stay aligned with best practices in data security.
FIrst of all, let’s add a new section to application.conf.
auth {
basic {
realm = "ktor"
usersFile = "users.properties"
}
}
- auth.basic: Serves as a root of our authentication config.
- realm: This is the name of the protected resource. Feel free to call it whichever way you like.
- usersFile: Relative path to the file with users’ credentials.
Next, we add users.properties containing usernames along with their respective passwords.
user1=secret1
user2=secret2
user3=secret3
Here is how src/main/resources
folder should look like at this stage.
Configure Security
Finally, we are ready to implement the validation of incoming requests against the stored credentials.
Note: The complete example is available on GitHub.
As a first step, we will create an extension method on Ktor’s Application
object. This method allows us to easily read user credentials from the text file and store them in-memory as key-value pairs in a map.
fun Application.loadUsers(filePath: String): Map<String, String> {
val userFile = this.javaClass.classLoader.getResource(filePath)
?: throw IllegalArgumentException("Could not read users file: $filePath")
return userFile.readText().lines().associate {
val (name, password) = it.split("=")
name to password
}
}
Next, we load and interpret application config.
fun Application.configureSecurity() {
// The relevant part of application configuration is the following:
val authConfig = environment.config.config("ktor.auth.basic")
// The realm is the name of the protected area:
val realm = authConfig.property("realm").getString()
// Usernames and passwords are read from the configuration file:
val usersFile = authConfig.property("usersFile").getString()
// Load the users from the file. In this example, the file is a simple text file:
val users = loadUsers(usersFile)
}
Finally, we update the configureSecurity
extension method with request validation.
install(Authentication) {
basic("auth-basic") {
this.realm = realm
validate { credentials ->
if (credentials.name in users &&
credentials.password == users[credentials.name]
) {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}
Ktor will verify each incoming request in accordance with the conditions specified in the validate
block. In this process, it extracts credentials from the Authorization header. Remember, these are base64 encoded. Ktor will decode them behind the scenes and construct the Credentials
object for you.
UPDATE: This is the final version of the security config:
fun Application.configureSecurity() {
// The relevant part of application configuration is the following:
val authConfig = environment.config.config("ktor.auth.basic")
// The realm is the name of the protected area:
val realm = authConfig.property("realm").getString()
// Usernames and passwords are read from the configuration file:
val usersFile = authConfig.property("usersFile").getString()
// Load the users from the file. In this example, the file is a simple text file:
val users = loadUsers(usersFile)
install(Authentication) {
basic("auth-basic") {
this.realm = realm
validate { credentials ->
if (credentials.name in users && credentials.password == users[credentials.name]) {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}
}
fun Application.loadUsers(filePath: String): Map<String, String> {
val userFile = this.javaClass.classLoader.getResource(filePath)
?: throw IllegalArgumentException("Could not read users file: $filePath")
val delimiter = "="
userFile.readText().lines()
return userFile.readText().lines().filter { it.contains(delimiter) }.associate {
val (name, password) = it.split(delimiter)
name to password
}
}
Protect Routes with Basic Authentication
To protect the desired routes we wrap them in an authenticate
block.
fun Application.configureRouting() {
routing {
authenticate("auth-basic") {
get("/") {
call.respondText("Hello World!")
}
}
}
}
Run It!
Let’s now test our implementation and see how basic authentication works to protect our resources. In your browser, navigate to http://localhost:8080. Here’s what you should expect.
When you close the dialogue generated by your browser, the page won’t load.
Entering the correct credentials unlocks access to the protected resource.
Inspect Network Traffic
Let’s have a closer look at communication between the client (browser) and the server.
Initially, when visiting http://localhost:8080
, the server responds with 401 Unauthorized
. Additionally, using the Www-Authenticate
header, it indicates the requirement on basic authentication. Note that the name of the protected realm is aligned with our security configuration.
The browser understands the protocol and displays a login popup. After having entered your credentials, the browser sends them encoded back to the server in the Authorization
header.
Keep in mind that credentials transmitted in plain text can be easily decoded. To prevent attackers from compromising user credentials, it is essential to use basic authentication in conjunction with HTTPS.
echo dXNlcjE6c2VjcmV0MQ== | base64 --decode
user1:secret1
Summary
Congratulations on completing this section of our hands-on tutorial. Today, you’ve gained an understanding of how basic authentication works. We’ve explored the negotiation process between the server and the browser, and discussed security risks related to transmitting user credentials essentially as plain text. Ktor greatly simplifies the process of incorporating basic authentication into a server. As we’ve seen enabling security with Ktor is easy and intuitive.
Thank you for reading! Stay tuned for our next episode, where we’ll enhance our minimalistic setup with form authentication.
The code you provide up to the headline “Run It!” generates an ApplicationConfigurationException: Property auth.basic.realm not found.
Hi Jonathan,
Thank you for letting me know! You’re right, the config path was wrong. It should be “ktor.auth.basic” as opposed to just “auth.basic”. Nevertheless, I’ve updated the article and included the full configuration. You can always check out the project from GitHub and run the complete example.