13 minutes read

CORS, or Cross-Origin Resource Sharing, is an important concept in web development that allows web developers to control how resources in one domain can be accessed by another domain. In this educational topic, we will explore the concept of CORS in detail, including its purpose, how it works, and how it can be installed and configured in web applications.

What is CORS?

Before you understand what CORS is, you need to deal with the SOP.

The Same Origin Policy (SOP) is a crucial security concept in web development that restricts the interaction between different web resources originating from various sources.

The "origin" in SOP refers to a combination of three things:

  • protocol

  • domain

  • port number

SOP dictates that web browsers should only allow scripts, cookies, and other resources to interact with pages from the same origin, preventing malicious scripts from accessing sensitive data on other web pages. The primary purpose of the SOP is to ensure the security and privacy of web users by preventing unauthorized access to their data.

For example, if we compare "https://mysite.com/firstPage" origin with others we will get the following results:

Origin

Result

Cause

https://mysite.com/secondPage

same origin

same protocol, same port and same domain

http://mysite.com/firstPage

different origin

same domain but different protocol

https://notmysite.com/secondPage

different origin

different domain

https://mysite.com:8080/secondPage

different origin

same protocol, but different port (https has 443 port by default)

However, the main weakness of SOP is that it can limit interactions between different web resources, making it difficult for web developers to create rich and interactive web applications. And to mitigate this limitation, the СORS is used.

Cross-Origin Resource Sharing (CORS) is a mechanism that allows web resources with one origin to be accessed by web pages with a different origin. CORS helps to relax the restrictions imposed by the Same Origin Policy (SOP) by defining a set of headers that enable a web server to allow cross-origin access to its resources.

For instance, imagine a news aggregator website. It's convenient to fetch articles from various news sources' APIs to display them on a single page. Without CORS, the browser would block these requests because they come from a different domain than the one hosting the web application.

the scheme of CORS requests

By default, web browsers block cross-origin requests, but CORS allows a web server to specify which origins are permitted to access its resources. This is achieved by adding additional headers such as Access-Control-Allow-Origin, which specifies the domains that are allowed to access the resource, or Access-Control-Allow-Methods, which specifies the HTTP methods that are allowed.

With the use of CORS, web developers can create rich and interactive web applications that can access resources from different domains, while still maintaining the security and privacy of web users.

Installation CORS Plugin

To use the CORS Plugin, you need to include the ktor-server-cors artifact in the build script:

implementation("io.ktor:ktor-server-cors:$ktor_version")

Then, pass the plugin to the install function inside the embeddedServer function call:

import io.ktor.server.application.*
import io.ktor.server.plugins.cors.*
// ...
fun main() {
    embeddedServer(Netty, port = 8080) {
        install(CORS)
    }.start(wait = true)
}

Or inside the explicitly defined module:

import io.ktor.server.application.*
import io.ktor.server.plugins.cors.*
// ...
fun Application.module() {
    install(CORS)
}

Also, you should know that you can install CORS Plugin to specific routes. That can be useful if you want a different СORS configuration for different routes.

Using CORS Plugin

Now, let's try to figure out the mechanism of CORS working using a simple example. To do this, create a simple server:

fun main() {
    embeddedServer(Netty, port = 8080, host = "localhost", module = Application::module).start(wait = true)
}

fun Application.module() {
    configureRouting()
}

And add such routing:

routing {
    get("/") {
        call.respondText("Hello world!")
    }

    post("/greet") {
        val name = call.receiveText()
        call.respondText("Hello, ${name}!")
    }
}

Now start the server, open the page on the path "/ " and then send the following fetch request from the browser console:

fetch(
  'http://localhost:8080/greet',
  {
    method: 'POST',
    body: 'User'
  }
).then(resp => resp.text()).then(console.log)

You will see "Hello, User!" response:

same origin request

Let's rebuild our server a bit to create a different origin:

fun main() {
    embeddedServer(Netty, applicationEngineEnvironment {
        connector {
            host = "0.0.0.0"
            port = 8080
        }
        connector {
            host = "0.0.0.0"
            port = 8081
        }
        module {
            configureRouting()
        }
    }).start(true)
}

And add routing for 8081 port inside our configureRouting function:

routing {
    port(8081) {
        get("/") {
            call.respondText("What are you waiting for?! Request from me!")
        }
    }
}

If you open "http://localhost:8081/" in the browser and try to send our fetch request, you'll get the following:

unsuccessful CORS request

To fix this, we need to install and configure CORS Plugin:

routing {
    port(8080) {
        install(CORS) {
            allowHost("127.0.0.1:8081")  // Instead of "127.0.0.1" insert your localhost address
        }

        post("/greet") {
            val name = call.receiveText()
            call.respondText("Hello, ${name}!")
        }
    }
}

Here we install CORS inside the routing block, but as we said, it's not the only option. To configure the plugin, we use the allowHost function to which we pass host and schemes arguments to allow cross-origin request from "http://localhost:8081/".

If you rerun the server and try again to send our request, you will see that now, it works!

If you look at the headers, now will see that a new header has appeared:

appearing of Access-Control-Allow-Origin header in response

That is a "simple" CORS request example. There are also "preflight" CORS requests, which involve the browser generating a preflight request using the OPTIONS method to check whether the server allows specific methods and headers to be used. Simple requests include requests using the GET, HEAD, and POST methods, headers automatically added by the browser, and a few other headers. You can learn more about this in detail in the related article. Also, you may face emerging additional (two preflights instead of one or preflights in simple requests) preflights, it's because of private network access (PNA).

It is important to keep in mind that when installing a CORS Plugin for specific routes (not in the embeddedServer function or module), it is necessary to add a handler for OPTIONS requests.

In addition, note that non-simple content types (simple content types include application/x-www-form-urlencoded, multipart/form-data, text/plain) require setting the allowNonSimpleContentTypes property to true.

It's not exhaustive information of using CORS, but that is the basic principle of CORS work. If you want to try different CORS requests configuration and get an explanation of them, visit "Will it CORS?".

Advanced configuring CORS Plugin

The CORS Plugin allows us to configure cross-origin requests using many parameters. Let's take a closer look at these parameters.

Hosts

The allowHost function can be used to specify which hosts are allowed to make cross-origin requests. In addition to the hostname, it is possible to specify a port number, a collection of subdomains, or supported HTTP schemes.

install(CORS) {
    allowHost("client-host:8081")    
    allowHost("client-host", subDomains = listOf("en", "de", "es"))
    allowHost("client-host", schemes = listOf("http", "https"))
}

To allow cross-origin requests from any host, use the anyHost function.

install(CORS) {
    anyHost()
}

Methods

The CORS plugin allows the use of the GET, POST, and HEAD HTTP methods by default. If you want to add more methods, you can use the allowMethod function.

install(CORS) {
    allowMethod(HttpMethod.Options)
    allowMethod(HttpMethod.Put)
    allowMethod(HttpMethod.Patch)
    allowMethod(HttpMethod.Delete)
}

Headers

The CORS plugin permits the following client headers, which are managed by Access-Control-Allow-Headers, as the default configuration:

  • Accept

  • Accept-Language

  • Content-Language

The allowHeader function can be used to enable extra headers.

install(CORS) {
    allowHeader(HttpHeaders.ContentType)
    allowHeader(HttpHeaders.Authorization)
}

The Access-Control-Expose-Headers header enables JavaScript in browsers to access the headers specified in the allowlist. To set up these headers, you can use the exposeHeader function.

install(CORS) {
    exposeHeader("X-My-Custom-Header")
    exposeHeader("X-Another-Custom-Header")
}

Credentials

Typically, browsers do not transmit credential information, such as authentication or cookies, when making cross-origin requests. To enable the transmission of this data, you can set the Access-Control-Allow-Credentials response header to true using the allowCredentials property.

install(CORS) {
    allowCredentials = true
}

This is not a comprehensive list of capabilities to customize your cross-origin interaction using the CORS plugin. You can learn more about them by exploring the CORSConfig class in detail.

Conclusion

In conclusion, let's highlight the following advantages of using CORS:

  • Access to external resources: An application using CORS can access resources hosted on different domains, while an application not using CORS is restricted by the same-origin policy. This means that an application using CORS can integrate with external services and APIs and provide more functionality to its users.

  • Improved security: While it may seem counterintuitive, using CORS can actually improve security by preventing malicious scripts from accessing resources they shouldn't. CORS helps prevent cross-site scripting (XSS) attacks by determining which domains are allowed to access resources.

  • Reduced server load: An application that uses CORS can offload some of its functionality to external services and APIs. This can reduce the load on the application's server, leading to better performance and scalability.

  • Flexibility: By allowing access to external resources, an application that uses CORS is more flexible and can easily adapt to changing requirements. For example, if a new service becomes available that could benefit the application's users, it can be integrated without requiring major changes to the application.

4 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo