Conquer Authentication with Ktor: Part 8 – Protect Access with CORS
Ensuring security and flexibility of web services when it comes to cross-origin resource sharing is essential. This is elegantly managed through the implementation of Cross-Origin Resource Sharing (CORS), an established practice for modern web applications. A well defined CORS policy not only enhances security but also promotes a seamless interaction between different domains. Thankfully, Ktor makes this process straightforward and efficient. In this final part of our series on authentication with Ktor, we will provide clear examples to guide you. By the end of this post, you’ll see how effortless it is to integrate CORS into your Ktor projects, ensuring your services are both secure and accessible.
This post is part of a hands-on tutorial. Feel free to check out the project from GitHub and follow along.
Table of Contents
- Installing Dependencies
- Configuring CORS in Ktor
- What is OPTIONS Request?
- CORS is Not a Silver Bullet
- Testing CORS with Ktor
- Summary
Installing Dependencies
CORS
ships as a plugin. Please add the following dependency to use it.
implementation("io.ktor:ktor-server-cors:$ktor_version")
As of time of this writing the project documentation refers to an older version of the plugin. Please note that io.ktor.server.plugins.cors.CORS
has been deprecated. The plugin has moved to a different package. This is what you need to import in your project:
import io.ktor.server.plugins.cors.routing.CORS
Configuring CORS in Ktor
You can configure CORS in Ktor by using the install(CORS)
feature. You can use this example as a referernce:
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.cors.routing.CORS
fun Application.module() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization)
allowCredentials = true
allowNonSimpleContentTypes = true
allowSameOrigin = true
anyHost() // Adjust this to suit your needs
}
}
This example
- enables selected HTTP methods
- allows the
Authorization
header - allows credentials. Browsers don’t send credentials or authentication cookies by default. Allowing credentials sets the
Access-Control-Allow-Credentials
response header totrue
. - allows non-simple content types. Without this setting our server won’t accept requests with JSON payload, for example. Only simple types (
text/plain
,application/x-www-form-urlencoded
andmultipart/form-data
) are allowed by default. - allows all hosts. Please make sure to adjust this to suit your needs.
The anyHost()
function is too permissive and it’s wise to replace it with the specific origin or the list of origins you want to allow. If you need to allow specific hosts only, please replace anyHost()
with allowHost()
configuration.
install(CORS) {
allowHost("www.example.com", schemes = listOf("http", "https"))
// Other configuration options
}
When using the install(CORS)
feature, Ktor automatically handles preflight OPTIONS
requests behind the scenes for you. Be sure to enable all the methods, headers, or any other configurations you wish to support in these preflight requests.
What is OPTIONS
Request?
In short, it’s a preflight request sent with the OPTIONS
method so that the server can respond if it is acceptable to send the actual request.
The HTTP OPTIONS
method is used by clients to find out what HTTP methods and other communication options are available for a specific URL endpoint on your server. When a client sends an OPTIONS
request, the server responds with information about the communication options available for the resource in question, such as supported HTTP methods (GET
, POST
, PUT
, DELETE
etc.) and CORS policies.
In the context of CORS, an OPTIONS
request serves as a preflight request that is sent before the actual request to determine if it makes sense to send the actual request. During this preflight, the browser can ask what methods are allowed when accessing a resource, or what non-simple content types can be used.
CORS is Not a Silver Bullet
Please note that CORS won’t prevent your server from receiving requests from unknown or malicious origins. It only advises the browser to either pass or drop the response to a request. Therefore, you should add other security measures, like CSRF tokens and checking the origin of non-idempotent requests.
Non-idempotent HTTP requests are those where making the same request multiple times can lead to different results or side effects. Simply put, repeated POST
requests typically create multiple resources in your system. Similarly, calling a PATCH
endpoint with different parameters modifies the underlying resources and changes its state every time. Whether the browser prevents or allows a response to such requests is irrelevant as the potential harm happens when such a request is processed by our server.
Testing CORS with Ktor
Can I verify that CORS policy works with unit tests? Absolutely! You can create unit tests in Ktor to verify that your setup is working as expected.
Here is an example:
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.http.HttpHeaders.AccessControlRequestMethod
import io.ktor.server.testing.*
import org.junit.Test
import kotlin.test.assertEquals
class CorsTest {
@Test
fun `test CORS options`() = testApplication {
application {
// Reuse the same CORS configuration
// as in the main Application module
}
client.options("/") {
header(HttpHeaders.Origin, "https://www.example.com")
header(AccessControlRequestMethod, "GET")
}.apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(
"https://www.example.com",
headers[HttpHeaders.AccessControlAllowOrigin]
)
assertEquals(
"true",
headers[HttpHeaders.AccessControlAllowCredentials]
)
}
}
}
In this test, an OPTIONS
request is sent along with an Origin
and Access-Control-Request-Method
header. The test checks that:
- the request is successful (status 200 OK)
- the
Access-Control-Allow-Origin
response header is correctly set, allowing requests fromhttps://www.example.com
- the
Access-Control-Allow-Credentials
response header is set totrue
. Meaning that cookies, HTTP authentication, and client-side SSL certificates are supported.
You could design similar tests to ensure that requests from unlisted origins are properly blocked, or that requests with unsupported methods are disallowed.
Remember to verify not only your CORS configuration but also the overall functioning and security of your application.
Summary
We’ve explored how CORS works and its role in modern web development. We’ve seen how simple it is to implement and test CORS within the Ktor framework. While relying solely on CORS does not guarantee full protection against various cyber attacks, ensuring the principles of CORS are applied within our projects is a step we must all take to safeguard our resources. As usual, the code examples are available on GitHub. Thanks for reading, hope you this post helped you get inspired to find out more about common security practices and how to achieve them with Ktor.