Why am I getting infinite redirect loop with force_ssl in my Rails app?

Ruby on-RailsRuby on-Rails-3RedirectSslNginx

Ruby on-Rails Problem Overview


I want to have my API controller use SSL, so I added another listen directive to my nginx.conf

upstream unicorn {
  server unix:/tmp/unicorn.foo.sock fail_timeout=0;
}

server {
  listen 80 default deferred;
  listen 443 ssl default;
  ssl_certificate /etc/ssl/certs/foo.crt;
  ssl_certificate_key /etc/ssl/private/foo.key;

  server_name foo;
  root /var/apps/foo/current/public;

  try_files $uri/system/maintenance.html $uri/index.html $uri @unicorn;

  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://unicorn;
  }

  error_page 502 503 /maintenance.html;
  error_page 500 504 /500.html;
  keepalive_timeout 5;
}

which passes the nginx conftest without any problems. I also added a force_ssl directive to my ApiController

class ApiController < ApplicationController
  force_ssl if Rails.env.production?

  def auth
    user = User.authenticate(params[:username], params[:password])
    respond_to do |format|
      format.json do
        if user
          user.generate_api_key! unless user.api_key.present?
          render json: { key: user.api_key }
        else
          render json: { error: 401 }, status: 401
        end
      end
    end
  end

  def check
    user = User.find_by_api_key(params[:api_key])
    respond_to do |format|
      format.json do
        if user
          render json: { status: 'ok' }
        else
          render json: { status: 'failure' }, status: 401
        end
      end
    end
  end
end

which worked just fine when I wasn't using SSL, but now when I try to curl --LI http://foo/api/auth.json, I get properly redirected to https, but then I keep on getting redirected to http://foo/api/auth ending in an infinite redirect loop.

My routes simply have

get "api/auth"
get "api/check"

I'm using Rails 3.2.1 on Ruby 1.9.2 with nginx 0.7.65

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

You're not forwarding any information about whether this request was an HTTPS-terminated request or not. Normally, in a server, the "ssl on;" directive will set these headers, but you're using a combined block.

Rack (and force_ssl) determines SSL by:

  • If the request came in on port 443 (this is likely not being passed back to Unicorn from nginx)
  • If ENV['HTTPS'] == "on"
  • If the X-Forwarded-Proto header == "HTTPS"

See the force_ssl source for the full story.

Since you're using a combined block, you want to use the third form. Try:

proxy_set_header X-Forwarded-Proto $scheme;

in your server or location block per the nginx documentation.

This will set the header to "http" when you come in on a port 80 request, and set it to "https" when you come in on a 443 request.

Solution 2 - Ruby on-Rails

Try setting this directive in your nginx location @unicorn block:

proxy_set_header X-Forwarded-Proto https;

I had this same issue and investigating the Rack middleware handler (not force_ssl but similar) I could see that it was expecting that header to be set to determine if the request was already processed as being SSL by nginx.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJakub ArnoldView Question on Stackoverflow
Solution 1 - Ruby on-RailsChris HealdView Answer on Stackoverflow
Solution 2 - Ruby on-RailsCody CaughlanView Answer on Stackoverflow