Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 845
Jakub Arnold Avatar asked Feb 25 '12 21:02

Jakub Arnold


1 Answers

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.

like image 130
Chris Heald Avatar answered Oct 16 '22 04:10

Chris Heald