25

I keep receiving this error message from the console when visiting my website:

font from origin 'https://xxx.cloudfront.net' has been blocked from loading by Cross-Origin Resource Sharing policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://www.example.com' is therefore not allowed access.

I've tried everything:

  • I've installed the font_assets gem
  • configured the application.rb file

    config.font_assets.origin = 'http://example.com'
    
  • Whitelisted Headers on Cloudfront as explained in this article to

    Access-Control-Allow-Origin
    Access-Control-Allow-Methods
    Access-Control-Allow-Headers
    Access-Control-Max-Age
    

But nothing, zero, nada.

I'm using Rails 4.1 on Heroku.

sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
Manuel F.
  • 251
  • 3
  • 5
  • Did you also add the `Origin:` header to the whitelist? – Michael - sqlbot Sep 15 '15 at 22:33
  • Hi Michael. No, should I? – Manuel F. Sep 15 '15 at 22:59
  • [UPDATE] I've changed it and the problem is still there. – Manuel F. Sep 15 '15 at 23:17
  • Your best bet will be to capture the request and response headers at the browser and server. Otherwise you are flying somewhat blind. You may also need to do a cache invalidation of the object in question, although changing the whitelist headers might do that implicitly. In fact, life might be easier if you temporarily select the option to forward *all* headers, which defeats the caching part of CloudFront -- caching will just make testing more complex. – Michael - sqlbot Sep 16 '15 at 02:05
  • Sorry Michael but I'm not sure what you mean. Caching is done only on production, no problems on development. – Manuel F. Sep 16 '15 at 12:05
  • You are running through CloudFront. Unless you are already explicitly disabling caching... there will be caching, done by CloudFront. – Michael - sqlbot Sep 16 '15 at 12:18
  • Ok, but I still don't understand how to fix my problem. Can you please guide me through? – Manuel F. Sep 16 '15 at 14:12
  • You will need to identify the source of the problem before it can be fixed, and the first step would be capturing request and response headers both at the browser and at the server, to identify where the incorrect behavior is occurring. It is not yet clear which part of your stack is causing the issue. – Michael - sqlbot Sep 16 '15 at 17:15
  • I've followed the Read.me of the font_assets gem and looked for Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age headers but they are not there. I'm quite sure that's the source of the problem. However, if it's a AWS configuration, I'm pretty stuck :) – Manuel F. Sep 17 '15 at 10:20
  • Are they there when you access the server directly, not through CloudFront? – Michael - sqlbot Sep 17 '15 at 10:56
  • It's weird. When I perform this call "curl -i https://www.example.com/assets/PN_Thin-ff116594019d5788ef116f40c0091cf537fee3840ef7160c0776571877773aa4.woff" this is what I get: HTTP/1.1 200 OK Connection: keep-alive Server: nginx/1.8.0 Date: Thu, 17 Sep 2015 13:12:34 GMT Content-Type: application/font-woff Content-Length: 54360 Last-Modified: Wed, 16 Sep 2015 21:20:33 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT Cache-Control: max-age=315360000 Cache-Control: public Accept-Ranges: bytes Via: 1.1 vegur – Manuel F. Sep 17 '15 at 13:14
  • but when I perform this call "curl -i -X OPTIONS https://www.example.com/assets/PN_Thin-ff116594019d5788ef116f40c0091cf537fee3840ef7160c0776571877773aa4.woff" this is what I get: HTTP/1.1 405 Not Allowed Connection: keep-alive Server: nginx/1.8.0 Date: Thu, 17 Sep 2015 13:13:39 GMT Content-Type: text/html Content-Length: 172 Via: 1.1 vegur – Manuel F. Sep 17 '15 at 13:17

7 Answers7

75

This was an incredibly difficult issue to deal with, for two reasons:

  1. The fact that CloudFront is mirroring our Rails app’s response headers requires you to twist your mind around. The CORS protocol is hard enough to understand as it is, but now you have to follow it at two levels: between the browser and CloudFront (when our Rails app uses it as a CDN), and between the browser and our Rails app (when some malicious site wants to abuse us).

    CORS is really about a dialog between the browser and the 3rd-party resources a web page wants to access. (In our use-case, that’s the CloudFront CDN, serving assets for our app.) But since CloudFront gets its Access-Control response headers from our app, our app needs to serve those headers as if it is CloudFront talking, and simultaneously not grant permissions that would expose itself to the type of abuse that led to the Same-Origin Policy / CORS being developed in the first place. In particular, we should not grant * access to * resources on our site.

  2. I found so much outdated information out there -- an endless line of blog posts and SO threads. CloudFront has improved its CORS support significantly since many of those posts, although it is still not perfect. (CORS should really be handled out-of-the-box.) And the gems themselves have evolved.

My setup: Rails 4.1.15 running on Heroku, with assets served from CloudFront. My app responds to both http and https, on both "www." and the zone apex, without doing any redirection.

I looked briefly at the font_assets gem mentioned in the question, but quickly dropped it in favor of rack-cors, which seemed more on point. I did not want to simply open up all origins and all paths, as that would defeat the point of CORS and the security of the Same-Origin Policy, so I needed to be able to specify the few origins I would allow. Finally, I personally favor configuring Rails via individual config/initializers/*.rb files rather than editing the standard config files (like config.ru or config/application.rb) Putting all that together, here is my solution, which I believe is the best available, as of 2016-04-16:

  1. Gemfile

    gem "rack-cors"
    

    The rack-cors gem implements the CORS protocol in a Rack middleware. In addition to setting Access-Control-Allow-Origin and related headers on approved origins, it adds a Vary: Origin response header, directing CloudFront to cache the responses (including the response headers) for each origin separately. This is crucial when our site is accessible via multiple origins (e.g. via both http and https, and via both "www." and the bare domain)

  2. config/initializers/rack-cors.rb

    ## Configure Rack CORS Middleware, so that CloudFront can serve our assets.
    ## See https://github.com/cyu/rack-cors
    
    if defined? Rack::Cors
        Rails.configuration.middleware.insert_before 0, Rack::Cors do
            allow do
                origins %w[
                    https://example.com
                     http://example.com
                    https://www.example.com
                     http://www.example.com
                    https://example-staging.herokuapp.com
                     http://example-staging.herokuapp.com
                ]
                resource '/assets/*'
            end
        end
    end
    

    This tells the browser that it may access resources on our Rails app (and by extension, on CloudFront, since it is mirroring us) only on behalf of our Rails app (and not on behalf of malicious-site.com) and only for /assets/ urls (and not for our controllers). In other words, allow CloudFront to serve assets but don't open the door any more than we have to.

    Notes:

    • I tried inserting this after rack-timeout instead of at the head of the middleware chain. It worked on dev but was not kicking in on Heroku, despite having the same middleware (other than Honeybadger).
    • The origins list could also be done as Regexps. Be careful to anchor patterns at the end-of-string.

      origins [
          /\Ahttps?:\/\/(www\.)?example\.com\z/,
          /\Ahttps?:\/\/example-staging\.herokuapp\.com\z/
      ]
      

      but I think it’s easier just to read literal strings.

  3. Configure CloudFront to pass the browser's Origin request header on to our Rails app.

    Strangely, it appears that CloudFront forwards the Origin header from the browser to our Rails app regardless whether we add it here, but that CloudFront honors our app’s Vary: Origin caching directive only if Origin is explicitly added to the headers whitelist (as of April 2016).

    The request header whitelist is kind of buried.

    If the distribution already exists, you can find it at:


    If you have not created the distribution yet, create it at:

    • https://console.aws.amazon.com/cloudfront/home#distributions
    • Click Create Distribution

      (For the sake of completeness and reproducibility, I'm listing all the settings I changed from the defaults, however the Whitelist settings are the only ones that are relevant to this discussion)

    • Delivery Method: Web (not RTMP)

    • Origin Settings

      • Origin Domain Name: example.com
      • Origin SSL Protocols: TLSv1.2 ONLY
      • Origin Protocol Policy: HTTPS only
    • Default Cache Behavior Settings

      • Viewer Protocol Policy: Redirect HTTP to HTTPS
      • Forward Headers: Whitelist
      • Whitelist Headers: Select Origin and click Add >>
      • Compress Objects Automatically: Yes

After changing all these things, remember that it can take some time for any old, cached values to expire from CloudFront. You can explicitly invalidate cached assets by going to the CloudFront distribution's Invalidations tab and creating an invalidation for *.

Noach Magedman
  • 2,313
  • 1
  • 23
  • 18
  • As a note, if you're still having issues, check you aren't locally cachine responses blindly. I had a django cache set and it was caching the CORS headers which CloudFront would then serve to the wrong domains – Rebs Apr 15 '16 at 06:34
  • 5
    @Noah: this is the best answer I've found out there. Thank you so much.. you have solved my problem. – fro_oo May 06 '16 at 06:48
  • 2
    Test it with curl: `curl -H "Origin: https://example.com" -I https://example.com/assets/your-font.ttf` – multipolygon May 11 '16 at 05:56
  • Thank you for this solution! A side note: Inspecting with chrome under the 'network' tab is handy. You can check its saying "vary:Origin" in the headers for the font resources. – Ed_ Oct 05 '16 at 02:11
  • before you embark on trying to get fonts to work with Cloudfront, it's worth noting this from the Amazon docs: "If you configure CloudFront to forward all headers to your origin, CloudFront doesn't cache the objects associated with this cache behavior. Instead, it sends every request to the origin" – Aaron Moodie Oct 31 '16 at 08:58
  • Aaron: Thanks for pointing that out, but in the instructions above, we are using 'Forward Headers: Whitelist', not 'Forward Headers: All' – Noach Magedman Oct 31 '16 at 21:20
  • Upvote for a great answer, but I don't think this solution can work with precompiled assets, since those are rendered from the `public` folder directly by your server (e.g. Apache) and bypass the Rack application altogether. I ended up just hosting my font files directly in my own S3 bucket, where I can control the CORS headers, and putting a CF distro in front of that. – Josh Leitzel May 18 '17 at 22:00
  • @JoshLeitzel You will need to set `config.serve_static_files` to true, so that you can set headers and serve assets by rails? – simo Aug 28 '17 at 17:06
  • 6
    This should be marked as correct answer. Just to help, Amazon has apparently buried even more the "Forward Header" option under "Cache Based on Selected Request Headers" – Matthieu Oct 12 '17 at 13:18
  • 1
    For Rails 5+, I believe this is a simpler solution? https://stackoverflow.com/a/45685644/74980 – GeekJock Nov 24 '18 at 20:13
  • I just tried that other method and it worked. I've reposted it in this thread as an answer: https://stackoverflow.com/a/53462112/74980 – GeekJock Nov 24 '18 at 20:31
  • After you add `rack-cors` to your app and redeploy, don't forget to invalidate your Cloudfront cache, otherwise it'd keep returning the old cached response without the necessary `access-control-allow-origin` header. – Jan Klimo May 21 '19 at 12:16
  • I also used the solution pointed to by @GeekJock and it worked for me on Rails 6 with Webpacker – Paul Odeon Nov 12 '20 at 20:17
8

If you run Rails on Passenger and Heroku: (if not, jump straight to Noach Magedman's answer)

Noach Magedman's answer was very useful for me to set up CloudFront properly.

I also installed rack-cors exactly as described and whilst it worked fine in development, the CURL commands in production never returned any of the CORS configurations:

curl -H "Origin: https://tidyme-staging.com.au" -I http://tidyme-staging.com.au/assets/31907B_4_0-588bd4e720d4008295dcfb85ef36b233ee0817d7fe23c76a3a543ebba8e7c85a.ttf

HTTP/1.1 200 OK
Connection: keep-alive
Server: nginx/1.10.0
Date: Wed, 03 Aug 2016 00:29:37 GMT
Content-Type: application/x-font-ttf
Content-Length: 316664
Last-Modified: Fri, 22 Jul 2016 03:31:57 GMT
Expires: Thu, 31 Dec 2037 23:55:55 GMT
Cache-Control: max-age=315360000
Cache-Control: public
Accept-Ranges: bytes
Via: 1.1 vegur

Note that I ping the server directly without going through the CDN, the CDN then after invalidating all content should just forward whatever the server responds. The important line here is Server: nginx/1.10.0, which indicates that assets are served by nginx and not Rails. As a consequence, the rack-cors configurations do not apply.

The solution that worked for us is here: http://monksealsoftware.com/ruby-on-rails-cors-heroku-passenger-5-0-28/

It basically involved cloning and modifying the nginx config file for Passenger, which is not ideal since this copy needs to be maintained every time Passenger gets upgraded and the template changes.

===

Here's a summary:

Navigate to the root folder of your Rails project and make a copy of the nginx config template

cp $(passenger-config about resourcesdir)/templates/standalone/config.erb config/passenger_config.erb

Open config/passenger_config.erb and comment this line out

<%# include_passenger_internal_template('rails_asset_pipeline.erb', 8, false) %>

Add these configurations below the line mentioned above

### BEGIN your own configuration options ###
# This is a good place to put your own config
# options. Note that your options must not
# conflict with the ones Passenger already sets.
# Learn more at:
# https://www.phusionpassenger.com/library/config/standalone/intro.html#nginx-configuration-template

location ~ "^/assets/.+\.(woff|eot|svg|ttf|otf).*" {
    error_page 490 = @static_asset_fonts;
    error_page 491 = @dynamic_request;
    recursive_error_pages on;

    if (-f $request_filename) {
        return 490;
    }
    if (!-f $request_filename) {
        return 491;
    }
}

# Rails asset pipeline support.
location ~ "^/assets/.+-([0-9a-f]{32}|[0-9a-f]{64})\..+" {
    error_page 490 = @static_asset;
    error_page 491 = @dynamic_request;
    recursive_error_pages on;

    if (-f $request_filename) {
        return 490;
    }
    if (!-f $request_filename) {
        return 491;
    }
}

location @static_asset {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
    add_header ETag "";
}

location @static_asset_fonts {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
    add_header ETag "";
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS';
    add_header 'Access-Control-Allow-Headers' '*';
    add_header 'Access-Control-Max-Age' 3628800;
}

location @dynamic_request {
    passenger_enabled on;
}

### END your own configuration options ###

Change the Procfile to include this custom config file

web: bundle exec passenger start -p $PORT --max-pool-size 2 --nginx-config-template ./config/passenger_config.erb

Then deploy...

===

If you know of a better solution, please put in the comments.

After implementing, the CURL command yielded the following response:

curl -H "Origin: https://tidyme-staging.com.au" -I http://tidyme-staging.com.au/assets/31907B_4_0-588bd4e720d4008295dcfb85ef36b233ee0817d7fe23c76a3a543ebba8e7c85a.ttf

HTTP/1.1 200 OK
Connection: keep-alive
Server: nginx/1.10.0
Date: Wed, 03 Aug 2016 01:43:48 GMT
Content-Type: application/x-font-ttf
Content-Length: 316664
Last-Modified: Fri, 22 Jul 2016 03:31:57 GMT
Expires: Thu, 31 Dec 2037 23:55:55 GMT
Cache-Control: max-age=315360000
Cache-Control: public
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
Access-Control-Allow-Headers: *
Access-Control-Max-Age: 3628800
Accept-Ranges: bytes
Via: 1.1 vegur
migu
  • 4,236
  • 5
  • 39
  • 60
6

As of version 5.0, Rails allows for setting custom HTTP Headers for assets and you don't have to use the rack-cors or font-assets gems. In order to set Access-Control-Allow-Origin for assets (including fonts), just add the following code to config/environments/production.rb:

config.public_file_server.headers = {
  'Access-Control-Allow-Origin' => '*'
}

The header value could also be a specific domain, like the following:

config.public_file_server.headers = {
  'Access-Control-Allow-Origin' => 'https://www.example.org'
}

This worked for my app and I didn't need to change any settings on Cloudfront.

GeekJock
  • 11,066
  • 13
  • 43
  • 44
2

I just had the same issue and managed to solve it.

You've correctly told Cloudfront to allow those headers, but you haven't added those headers to where Cloudfront gets the font. Yes, your origin headers are allowed, but Heroku isn't sending those headers with the font anyway.

To fix this, you'll need to get the proper CORS headers added to the font on Heroku. Luckily, this is pretty easy.

First, add the rack/cors gem to your project. https://github.com/cyu/rack-cors

Next, configure your Rack server to load and configure CORS for any assets it serves. Add the following after your application preloads in config.ru

require 'rack/cors'
use Rack::Cors do
  allow do
    origins '*'

    resource '/cors',
      :headers => :any,
      :methods => [:post],
      :credentials => true,
      :max_age => 0

    resource '*',
      :headers => :any,
      :methods => [:get, :post, :delete, :put, :patch, :options, :head],
      :max_age => 0
    end
  end

This sets any resources returned from Heroku to have the proper CORS headers applied. You can restrict the application of headers depending on your file and security needs.

Once deployed, go into Cloudfront and begin an invalidation on anything that was previously giving you a CORS permission error. Now when Cloudfront loads a fresh copy from Heroku, it will have the correct headers, and Cloudfront will pass those headers on to the client as previously configured with your Origin permissions.

To make sure you're serving the proper headers from your server, you can use the following curl command to validate your headers: curl -I -s -X GET -H "Origin: www.yoursite.com" http://www.yoursite.dev:5000/assets/fonts/myfont.svg

You should see the following headers returned:

Access-Control-Allow-Origin: www.yoursite.com
Access-Control-Allow-Methods: GET, POST, DELETE, PUT, PATCH, OPTIONS, HEAD
Access-Control-Max-Age: 0
Access-Control-Allow-Credentials: true
Joe Burns
  • 21
  • 4
0

Here is a repo the demonstrates serving a custom font with Rails 5.2 that works on Heroku. It goes further and optimizes serving the fonts to be as fast as possible according to https://www.webpagetest.org/

https://github.com/nzoschke/edgecors

Asset Pipeline and SCSS

  • Place fonts in app/assets/fonts
  • Place the @font-face declaration in an scss file and use the font-url helper

From app/assets/stylesheets/welcome.scss:

@font-face {
  font-family: 'Inconsolata';
  src: font-url('Inconsolata-Regular.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
}

body {
  font-family: "Inconsolata";
  font-weight: bold;
}

Serve from CDN with CORS

I'm using CloudFront, added with the Heroku Edge addon.

If you're using your own CloudFront, make sure to configure it to forward the browser Origin header to your backend origin.

First configure a CDN prefix and default Cache-Control headers in production.rb:

Rails.application.configure do
  # e.g. https://d1unsc88mkka3m.cloudfront.net
  config.action_controller.asset_host = ENV["EDGE_URL"]

  config.public_file_server.headers = {
    'Cache-Control' => 'public, max-age=31536000'
  }
end

If you try to access the font from the herokuapp.com URL to the CDN URL, you will get a CORS error in your browser:

Access to font at 'https://d1unsc88mkka3m.cloudfront.net/assets/Inconsolata-Regular.ttf' from origin 'https://edgecors.herokuapp.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. edgecors.herokuapp.com/ GET https://d1unsc88mkka3m.cloudfront.net/assets/Inconsolata-Regular.ttf net::ERR_FAILED

So configure CORS to allow access to the font from Heroku to the CDN URL:

module EdgeCors
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    config.middleware.insert_after ActionDispatch::Static, Rack::Deflater

    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins %w[
          http://edgecors.herokuapp.com
          https://edgecors.herokuapp.com
        ]
        resource "*", headers: :any, methods: [:get, :post, :options]
      end
    end
  end
end

Serve gzip Font Asset

The asset pipeline builds a .ttf.gz file but doesn't serve it. This monkey patch changes the asset pipeline gzip whitelist to a blacklist:

require 'action_dispatch/middleware/static'

ActionDispatch::FileHandler.class_eval do
  private

    def gzip_file_path(path)
      return false if ['image/png', 'image/jpeg', 'image/gif'].include? content_type(path)
      gzip_path = "#{path}.gz"
      if File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
        gzip_path
      else
        false
      end
    end
end

The ultimate result is a custom font file in app/assets/fonts served from a long-lived CloudFront cache.

Noah Zoschke
  • 571
  • 4
  • 4
0

Probably best is to use rack-cors gem. In the spirit of do-it-yourself and a follow-up to @GeekJock's answer. If one doesn't want to use rack-cors gem, this is poor man's CORS headers handling for a situation where for example we care only about static fonts assets (e.g. replacing the font_assets obsoleted gem).

Like in the other answer, you put in app config:

config.public_file_server.headers = {
  'Access-Control-Allow-Origin' => '*'
}

To handle OPTIONS pre-flight requests, you can write a route matcher somewhere under /lib:

module FontAssetsConstraint
  FONT_EXTENSIONS = %w[eot svg ttf otf woff woff2].freeze

  module_function

  def matches?(request)
    extension = request.params["format"]
    extension.present? && FONT_EXTENSIONS.include?(extension)
  end
end

And then add a route definition config/routes.rb to catch those to reply:

Rails.application.routes.draw do
  # Respond to pre-flight CSRF requests for font assets
  if Rails.configuration.public_file_server.enabled &&
     Rails.configuration.public_file_server.headers.include?("Access-Control-Allow-Origin")
    constraints FontAssetsConstraint do
      match "*path", via: :options, to: ->(hash) { [204, Rails.configuration.public_file_server.headers, []] }
    end
  end

Alternatively to writing a route matcher and a definition you can create your own middleware to catch fonts:

class AssetsOptionsResponder
  TYPES = %w(eot svg ttf otf woff woff2).freeze

  def initialize(app)
    @app = app
  end

  def call(env)
    if env["REQUEST_METHOD"] == "OPTIONS" && targeted?(env["PATH_INFO"])
      [204, access_control_headers, []]
    else
      @app.call(env)
    end
  end

  private

  def targeted?(pathinfo)
    return if pathinfo.blank?
    TYPES.include? extension(pathinfo)
  end

  def extension(pathinfo)
    pathinfo.split("?").first.split(".").last
  end

  def access_control_headers
    Rails.configuration.public_file_server.headers
  end
end

Then in app config or an initializer, you can add this middleware:

Rails.application.configure do
  if defined?(ActionDispatch::Static) &&
     Rails.configuration.public_file_server.enabled &&
     Rails.configuration.public_file_server.headers.include?("Access-Control-Allow-Origin")
    config.middleware.insert_before ActionDispatch::Static, AssetsOptionsResponder
  end
end
akostadinov
  • 17,364
  • 6
  • 77
  • 85
0
# Gemfile
gem 'rack-cors'

# config/initializers/cors.rb

## very permissive
origins '*'
resource '*', headers: :any, methods: [:get]

## example of specifying only what's necessary
origins 'app.example.com'
resource '/packs/*', headers: :any, methods: [:get] # webpack
resource '/assets/*', headers: :any, methods: [:get] # asset pipeline
John Bachir
  • 22,495
  • 29
  • 154
  • 227