Last Updated: December 31, 2020
·
11.45K
· cybersamx

Serving protected static content using nginx for speed and Rails server for authentication

Problem

A user needs to access a protected static content (eg. image) that must be served in https by a web server (for much faster download speed) only after the request has been successfully authenticated and authorized by a Rails server.

The Setup

Let's assume the following:

  • Nginx: nginx is running as a web and reverse proxy server. All requests to the rails server are passed onto port 3000 (ie. the default port for the Rails server)
  • Thin: rails server running on port 3000
  • Rails directory: the rails project root directory is located at /var/rails/rails_app
  • Directory where the protected static resources is called:/var/rails/rails_app/images_fs_dir
  • URL to access the protected static resources: http://myhost.com/images_uri_dir

Solution

Why use nginx to serve the static content?

Because nginx is 5-20x faster than thin when it comes to serving static content.

Logic flow:

  • Request is made to http://myhost.com/images_uri_dir/companyA/bar_chart.png
  • Request gets redirected to https://myhost.com/images_uri_dir/companyA/bar_chart.png (see rewrite ^ https://$host$request_uri permanent)
  • Nginx receives the request, matches the request path with it's mapping rules in nginx.conf, adds HTTP headers X-Accel-Redirect and X-Accel-Mapping, and sends the request to http://localhost:3000/images_uri_dir/companyA/bar_chart.png (see proxy_pass in nginx.conf)
  • Thin receives the request, looks up routes.rb and routes the request to ImagesController#show
  • The before_filter method in the controller performs the appropriate authentication and authorization
  • config/environments/production.rb tells the rails server to look for X-Accel-Redirect and X-Accel-Mapping in a request
  • Rails compares the file path that is passed to sendfile with what is defined in the left-hand-side of the X-Accel-Mapping value (ie. /var/rails/railsapp/=/imagesfsdir/). If the 2 values match, the controller passes the request back to nginx. Otherwise the controller will send the file back to the client
  • Nginx receives the request and looks at the right-hand-side of X-Accel-Mapping value (ie. /var/rails/railsapp/=/imagesfs_dir/). Since the request is internal to the host, nginx loads and sends the file back to the client (see internal in nginx.conf)

The Rails project root directory:

$ cd /var/rails/rails_app
$ ls
images_fs_dir
$ ls -R  # List files recursively
.:
customerA  customerB

./customerA
bar_chart.png
pie_chart.png

./customerB
bar_chart.png
pie_chart.png
...

nginx file should look like this:

events {
  worker_connections 1024;
}

http {
  include /etc/nginx/mime.types;
  index index.html index.htm;

  default_type application/octet-stream;

  upstream rails {
    server localhost:3000;
  }

  server {
    listen 80;
    server_name localhost;

    # Direct all http requests to https.
    location / {
      rewrite ^ https://$host$request_uri permanent;
    }
  }

  server {
    listen 443 default_server ssl;
    server_name localhost;
    root /var/rails/rails_app;

    ssl on;
    ssl_certificate /etc/ssl/certs/my_cert.crt;
    ssl_certificate_key /etc/ssl/keys/my_key.key;

    # Protected directory
    # Note: rails will handle /images_uri_dir requests
    location /images_fs_dir/ {    
      alias /var/rails/rails_app/;  # Append the path with /
      internal;   # Can't access this directory from direct access from the web
    }

    location / {
      proxy_redirect    off;

      proxy_set_header  Host             $http_host;
      proxy_set_header  X-Real-IP        $remote_addr;
      proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;

      proxy_set_header  X-Sendfile-Type  X-Accel-Redirect;
      proxy_set_header  X-Accel-Mapping  /var/rails/rails_app/=/images_fs_dir/;

      # Make sure you open and close your X-Accel-Mapping value with /

      proxy_pass http://rails;
    }
  }
}

config/routes.rb should look like this:

# ...

get '/images_uri_dir/:customer_id/:category.png' => 'images#show'

#...

config/environments/production.rb should look like this:

# ...

# This redirects the request back to nginx to load and serve the static content after a success authentication/authorization

RailsApp::Application.configure do
  config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
  config.middleware.insert(0, Rack::Sendfile, config.action_dispatch.x_sendfile_header)

# ...

app/controllers/images_controller.rb

# ...

before_filter :validate_user

def validate_user
  # Authentication and authorization logic here
end

def show
  file_path = "#{Rails.root}/images_fs_dir/#{customer_id}/#{category}.png"

  send_file(file_path, type: 'image/png', disposition: 'inline')
end

def images_params
  params.permit(:customer_id, :category)
end

# ...