mechanical bee logo

about us

We make web applications for ourselves and for clients. or learn more.

Omniauth, openid, heroku, and https

We recently added the option to log in or sign up with Facebook, Twitter, or Google using the handy omniauth gem.

Facebook and Twitter went swimmingly. We had already been integrating with Facebook and Twitter so that people could tweet or post to Facebook to tell their friends about the items they’re giving away. Adding a sign up/log in pathway was just an extension of that functionality.

But we ran into a problem with Google and OpenID: they worked fine in development but kept failing with “invalid credentials” in production.

There are two main differences with our production environment:

  1. It’s on Heroku which has a read-only system
  2. It always runs over https

We looked at possible problems with writing to the filesystem on Heroku first. Even though we had the filesystem path set to ./tmp, we were still worried about it and thought about switching to a memcache store. But then we tested another app on Heroku with the same filesystem store writing to ./tmp and that worked fine.

Okay maybe it was a problem with ssl. This turned out to be really hard to google because we kept trying some combination of “Omniauth openid heroku https” (thus the blatant google mongering of this title) but in fact the problem was with Rack. We’re using rails 3.0.3, omniauth 0.2.5, devise 1.3.4, and rack 1.2.2. The 1.3.0.beta version of rack has a notably different request.rb from the 1.2.2 version. Notice the scheme and port methods.

request.rb in rack 1.2.2:

def scheme;          @env["rack.url_scheme"]                  end
def port;            @env["SERVER_PORT"].to_i                 end

request.rb in rack 1.3.0.beta:

def scheme
  if @env['HTTPS'] == 'on'
    'https'
  elsif @env['HTTP_X_FORWARDED_SSL'] == 'on'
    'https'
  elsif @env['HTTP_X_FORWARDED_PROTO']
    @env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
  else
    @env["rack.url_scheme"]
  end
end

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

When we go to /auth/google in production, rack 1.2.2 doesn’t realize we’re coming from https and the return_to for OpenID gets set to http. In development, this was fine because everything is http but in production, OpenID does a verification on the return_to url and raises a ProtocolError when the schemes don’t match.

idres.rb:199:

[:scheme, :host, :port, :path].each do |meth|
  if msg_return_to.send(meth) != app_parsed.send(meth)
    raise ProtocolError, "return_to #{meth.to_s} does not match"
  end
end

Then the OpenID strategy in omniauth fails with the not so useful “invalid credentials” message.

open_id.rb:91

@openid_response = env.delete('rack.openid.response')
if @openid_response && @openid_response.status == :success
  super
else
  fail!(:invalid_credentials)
end

We ended up monkeypatching it with this code and it seems to be working so far.