As a SW/Ops/DB Engineer

riywo’s technology memo

Omniauth-openid With Pow and Nginx SSL Termination

I tried to use omniauth-openid with my new rails application. It worked when I developed using HTTP. After I started to develop with HTTPS, it failed. Finally I got the coolest workaround: OmniAuth.config.full_host and X-Forwarded-Host/Port.

My Environment

I use Pow for a development server. To use HTTPS with Pow, I installed nginx and configured like below.

server {
    listen     443;
    server_name  *.dev *.xip.io;

    ssl               on;
    ssl_certificate   ssl/my.crt;
    ssl_certificate_key  ssl/my.key;

    ssl_session_timeout  5m;

    ssl_protocols  SSLv2 SSLv3 TLSv1;
    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers   on;

    location / {
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto https;
      proxy_redirect off;
      proxy_pass http://127.0.0.1;
      proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
    }
}

For authentication, I use omniauth-openid via omniauth-google-apps and devise.

config/initializers/devise.rb

config.omniauth :google_apps, store: OpenID::Store::Filesystem.new('/tmp'), domain: ENV['GOOGLE_APPS_DOMAIN']

app/controllers/users/omniauth_callbacks_controller.rb

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  skip_before_action :verify_authenticity_token

  def google_apps
      …

They worked if I access to Pow directly using HTTP.

What was the problem 1?

I guess all people using omniauth-oauth2 behind a reverse proxy encounter “Redirect uri mismatch” error. Because redirect_uri query string parameter is like https://myapp.127.0.0.1.xip.io:80/users/auth/github. :80 ?????

And omniauth-google-apps didn’t work neither. My browser tried to redirect to https://myapp.127.0.0.1.xip.io:80/users/auth/google_apps/callback

Solution

Here is a simple workaround for this.

To simplify, I configured it as a string.

OmniAuth.config.full_host = "https://myapp.127.0.0.1.xip.io"

What was the problem 2?

After that, when I accessed to https://myapp.127.0.0.1.xip.io/users/auth/google_apps, Google said:

Error: invalid_request
Error in parsing the OpenID auth request.

The reason was a request query string parameter:

openid.realm:https://myapp.127.0.0.1.xip.io:80

:80 again… At this time, it came from rack-openid via omniauth-openid.

rack-openid/lib/rack/openid.rb at v1.3.1 · josh/rack-openid

 oidreq.redirect_url(trust_root || realm_url(req), return_to || request_url, immediate)

At first, I applied a monkey patch to omniauth-openid:

--- a/lib/omniauth/strategies/open_id.rb
+++ b/lib/omniauth/strategies/open_id.rb
@@ -32,6 +32,7 @@ module OmniAuth
         lambda{|env| [401, {"WWW-Authenticate" => Rack::OpenID.build_header(
           :identifier => identifier,
           :return_to => callback_url,
+         :trust_root => full_host,
           :required => options.required,
           :optional => options.optional,
           :method => 'post'

Then, opened.realm looked fine, but after the callback, OmniAuth said:

Could not authenticate you from GoogleApps because "Invalid credentials".

This was because rack-openid doesn’t pay attention to trust_root.

rack-openid/lib/rack/openid.rb at v1.3.1 · josh/rack-openid

consumer.complete(flatten_params(req.params), req.url)

Then, I found another monkey patch for Rack::Request.

But this is not cool. I thought that Rack::Request must have a functionality to deal with HTTPS reverse proxy.

rack/lib/rack/request.rb at 1.5.2 · rack/rack

def port
  if port = host_with_port.split(/:/)[1]
    port.to_i
  elsif port = @env['HTTP_X_FORWARDED_PORT']
    port.to_i
  elsif @env.has_key?("HTTP_X_FORWARDED_HOST")
    DEFAULT_PORTS[scheme]
  else
    @env["SERVER_PORT"].to_i
  end
end

YES!!!!!!!!!!!!!!!

Solution

I added X-Forwarded-Host on my nginx.conf, then it worked! Because I had already configured X-Forwarded-Proto.

proxy_set_header X-Forwarded-Host $host;

Or X-Forwarded-Port also worked.

proxy_set_header X-Forwarded-Port 443;

Conclusion

If you use SSL termination(nginx, Apache, LB, etc.) with your application:

  • You should use SSL termination for your development environment.
    • Otherwise, you will have some trouble between dev and prod…
  • You should pass much information using X-Forwarded-* headers.
    • Many libraries handle these headers better than you:)

Comments