Software Development
Krzysztof Buszewicz, 2021-04-01

Rails API & CORS. A dash of consciousness

For an experienced developer, this text may not be surprising at all, but I think that plenty of articles I’ve read about the CORS setup in Rails were saying something like: use rack-cors, allow any host to access the API, and (optionally): you should consider something different (than allowing any host) in production.

The proposed code was always close to the one below:

# config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: :any
  end
end

and, unfortunately, these texts were hardly explaining to us what to actually do in production.

I’m pretty OK with copy-pasting (I’m sometimes joking that companies could hire a Stack Overflow copy-paster), as far as there’s a “think and adjust” moment between “copy” and “paste”. So, I’d like to elaborate a little bit on what we’re doing here and how it works in real life.

I hope you don’t mind me starting with a short introduction to honor theory and then passing on to the Rails examples.

Introduction

Let’s start from the beginning. To explain things better, I’ve split the introduction into three parts. The first part will outline what’s an origin – the key term for what we are discussing here. The second is about SOP, just a short description. And the last part speaks about the CORS itself.

What is an origin?

According to the MDN Web Docs:

- Web content's origin is defined by the scheme (protocol), host (domain), and port of the URL used to access it. Two objects have the same origin only when the scheme, host, and port all match (source)

That seems pretty clear, doesn’t it? Let’s analyze two examples from MDN, just in case.

  1. http://example.com/app1/index.html, http://example.com/app2/index.html

The 2 above have the same origin because:

  • their schemes (http) are the same,
  • their domains (example.com) are the same,
  • their ports (implicit) are the same.
  1. http://www.example.com, http://myapp.example.com

These 2 have different origin because the domains (www.example.com, myapp.example.com) are different.

I hope it’s clear enough. If not, please go to the MDN Web Docs for more examples.

Code Review

What is SOP?

MDN Web Docs say (source):

- The same-origin policy is a critical security mechanism that restricts how a document or script loaded from one origin can interact with a resource from another origin. It helps isolate potentially malicious documents, reducing possible attack vectors.

- Cross-origin writes are typically allowed. Examples are links, redirects, and form submissions.

- Cross-origin embedding is typically allowed.

- Cross-origin reads are typically disallowed, but read access is often leaked by embedding.
Use CORS to allow cross-origin access

Well, as you can see, there is a lot about cross-origin behavior in the definitions of SOP. That’s ok. All we should know now is that the same origin has more privileges and we can loosen the rules for cross-origins by using CORS. And here the next section comes in.

What is CORS?

Basing on MDN’s words:

  • Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any other origins (domain, scheme, or port) than its own from which a browser should permit loading of resources. CORS also relies on a mechanism by which browsers make a “preflight” request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request (source).

That’s still not enough. What was not said there explicitly is that the most important header when using CORS is Access-Control-Allow-Origin:

  • The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin (source).

Well, that should be it. In real life, when configuring CORS, we typically configure the ACAO header first.

Real Life

That’s it when it comes to definitions. Let’s circle back to Rails and real-life examples.

How to configure CORS in Rails?

We will definitely use rack-cors (like we were told to). Let’s recall the first snippet, the one that is most often provided in other articles:

# config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: :any
  end
end

The number of options is vast or even infinite but let’s consider those two:

  • we’re building the API that is allowed to be used by third party browser clients,
  • we’ve typical frontend/backend separation and want to allow our trusted clients to access the API.

Building API accessed by third party clients

If you’re facing the first option, you probably could go with origins '*' – you want others to build a client on the top of your API, and don’t know who they are, right?

Typical frontend/backend separation

If you are developing the latter, you probably don’t want everyone to make cross-origin requests to your API. You rather want to:

  • allow production clients to access production API,
  • same for staging,
  • same for localhost,
  • you may want to allow FE review apps to access staging.

We will be still using rack-cors (like we were told to) – but our way.

Let’s use 2 ENV variables: ALLOWED_ORIGINS for literal origin definitions (an asterisk or actual URL) and ALLOWED_ORIGIN_REGEXPS for the patterns.

# config/initializers/cors.rb
# frozen_string_literal: true

to_regexp = ->(string) { Regexp.new(string) }
hosts = [
  *ENV.fetch('ALLOWED_ORIGINS').split(','),
  *ENV.fetch('ALLOWED_ORIGIN_REGEXPS').split(';').map(&to_regexp)
]

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins(*hosts)

    resource '*',
             methods: %i[get post put patch delete options head],
             headers: :any
  end
end

What’s going on here?

  1. As you can see, we’re splitting the values defined in ENV variables with different separators. That’s because a semicolon is less likely to appear in the URL defining pattern.
  2. Literal values are ready for use, but we have to map the patterns to be actual Regexp instances.
  3. Then, we’re joining everything together and allowing these hosts to access any resource with whitelisted methods our API uses.

This should give you enough flexibility to define proper values in your development, staging and production environments.

Conclusions

Let’s sum up all of the above in key points:

  • use ENV variables to configure CORS,
  • use regular expressions to allow different origins to access staging API (e.g., for review apps),
  • always put “think and adjust” between “copy” and “paste”.

That’s it. Have a nice day! :)

Product Development

Read more:

Why you should (probably) use Typescript?

The Codest is recognized by Techreviewer as a Top Software Development company in 2021

10 NYC Startups worth mentioning in 2021