Friday, May 25, 2012

OmniAuth & Facebook: certificate verify failed


I've followed Railscast #235 to try and set up a minimal Facebook authentication.



I've first set up a Twitter authentication, as done by Ryan himself. That worked flawlessly.



I then moved on to adding a Facebook login. However, after authorizing the app the redirect to /auth/facebook/callback fails with:




SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed



I am working on localhost. I didn't set up any SSL within the app. What am I doing wrong?


Source: Tips4all

14 comments:

  1. The real problem is that Faraday (which Omniauth/Oauth use for their HTTP calls) is not wasn't setting the ca_path variable for OpenSSL. At least on Ubuntu, most root certs are stored in "/etc/ssl/certs". Since Faraday isn't wasn't setting this variable (and currently does not have a method to do so), OpenSSL isn't wasn't finding the root certificate for Facebook's SSL certificate.

    I've submitted a pull request to Faraday which will add support for this variable and hopefully they will pull in this change soon. Until then, you can monkeypatch faraday to look like this or use my fork of Faraday. After that, you should specify version 0.3.0 of the OAuth2 gem in your Gemspec which supports the passing of SSL options through to Faraday. All you need to do now is upgrade to Faraday 0.6.1, which supports passing of the ca_path variable and upgrade to OmniAuth 0.2.2, which has the proper dependencies for OAuth2. You'll then be able to properly fix this issue by just adding the following to your Omniauth initializer:

    Rails.application.config.middleware.use OmniAuth::Builder do
    provider :facebook, FACEBOOK_KEY, FACEBOOK_SECRET, {:client_options => {:ssl => {:ca_path => "/etc/ssl/certs"}}}
    end


    So, to recap:


    Faraday needs to be updated to support SSL ca_path. Install Faraday 0.6.1
    Your app needs to use OAuth2 version 0.3.0. You may need to fork omniauth since it currently has a minor version dependency in the 0.2.x tree. Upgrade to OmniAuth 0.2.2
    Modify your provider initializer to point to your system's certificate path ("/etc/ssl/certs" on Ubuntu et al)


    Hopefully the next releases of both Faraday and Omniauth will incorporate this solution.

    Thanks to KirylP above for setting me on the right path.

    ReplyDelete
  2. I was having this problem and tried using the :ca_path argument without success. After looking through Github for awhile, I came across a suggestion that mentioned using :ca_file and point directly to the certification.

    Rails.application.config.middleware.use OmniAuth::Builder do
    provider :facebook, 'secret_key', 'secret_key',
    :client_options => {:ssl => {:ca_file => '/etc/pki/tls/certs/ca-bundle.crt'}}}
    end


    If you need to get the path to your systems certification files (and your using linux) simply type from the terminal. This will give you a bunch of information about your SSL setup, including the path (refer to OPENSSLDIR). You'll need to add certs/ca-bundle.crt to the path provided.

    open-ssl version -a

    ReplyDelete
  3. Managed to go through SSL Certificate Verification like it has to be.
    My project is using 37signals ID for Basecamp integration (Ruby 1.9.2-p130, Rails 3.0.4).

    RAILS_ROOT/config/initializers/omniauth.rb:

    require 'omniauth/oauth'

    Rails.application.config.middleware.use OmniAuth::Strategies::ThirtySevenSignals,
    'CLIENT_ID', 'CLIENT_SECRET', {client_options: {ssl: {ca_file: Rails.root.join('gd_bundle.crt').to_s}}}

    module OAuth2
    class Client
    def initialize(client_id, client_secret, opts = {})
    adapter = opts.delete(:adapter)
    self.id = client_id
    self.secret = client_secret
    self.site = opts.delete(:site) if opts[:site]
    self.options = opts
    self.connection = Faraday::Connection.new(site, {ssl: opts.delete(:ssl)})
    self.json = opts.delete(:parse_json) # ^ my code starts here

    if adapter && adapter != :test
    connection.build { |b| b.adapter(adapter) }
    end
    end
    end
    end


    Where 'CLIENT_ID', 'CLIENT_SECRET' you can get at 37signals.com and certificates bundle file gd_bundle.crt from GoDaddy because 37signals are using their CA.

    ReplyDelete
  4. Looks like Omniauth now uses a newer version of Faraday, which explains why the monkey patch above wasn't working for me. I agree there must be a better way, but for anyone else who just needs to get it working to test, here's an updated version:

    (create a file in your initializers directory with the following code)

    require 'faraday'
    module Faraday
    class Adapter
    class NetHttp < Faraday::Adapter
    def call(env)
    super
    url = env[:url]
    req = env[:request]

    http = net_http_class(env).new(url.host, url.inferred_port)

    if http.use_ssl = (url.scheme == 'https' && env[:ssl])
    ssl = env[:ssl]
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    http.cert = ssl[:client_cert] if ssl[:client_cert]
    http.key = ssl[:client_key] if ssl[:client_key]
    http.ca_file = ssl[:ca_file] if ssl[:ca_file]
    http.cert_store = ssl[:cert_store] if ssl[:cert_store]
    end

    http.read_timeout = http.open_timeout = req[:timeout] if req[:timeout]
    http.open_timeout = req[:open_timeout] if req[:open_timeout]

    if :get != env[:method]
    http_request = Net::HTTPGenericRequest.new \
    env[:method].to_s.upcase, # request method
    !!env[:body], # is there data
    true, # does net/http love you, true or false?
    url.request_uri, # request uri path
    env[:request_headers] # request headers

    if env[:body].respond_to?(:read)
    http_request.body_stream = env[:body]
    env[:body] = nil
    end
    end

    begin
    http_response = if :get == env[:method]
    # prefer `get` to `request` because the former handles gzip (ruby 1.9)
    http.get url.request_uri, env[:request_headers]
    else
    http.request http_request, env[:body]
    end
    rescue Errno::ECONNREFUSED
    raise Error::ConnectionFailed, $!
    end

    http_response.each_header do |key, value|
    response_headers(env)[key] = value
    end
    env.update :status => http_response.code.to_i, :body => http_response.body

    @app.call env
    end
    end
    end
    end

    ReplyDelete
  5. If you are deploying to Heroku, you want to point to the specific file location. This works for me (in config/initializers/omniauth.rb):

    Rails.application.config.middleware.use OmniAuth::Builder do
    # This cert location is only for Heroku
    provider :facebook, APP_ID, APP_SECRET, {:client_options => {:ssl => {:ca_file => "/usr/lib/ssl/certs/ca-certificates.crt"}}}
    end

    ReplyDelete
  6. I solved this with CA bundle from: http://certifie.com/ca-bundle/

    And in my Devise initializer:

    :client_options => { :ssl => { :ca_file => "#{Rails.root}/config/ca-bundle.crt" } } }

    ReplyDelete
  7. Edit: Check the answer below as it is more relevant

    This worked for me (fix courtesy of https://github.com/jspooner):

    Create a file in your initializer's directory with the following monkey patch:

    require 'faraday'
    module Faraday
    class Adapter
    class NetHttp < Faraday::Adapter
    def call(env)
    super

    is_ssl = env[:url].scheme == 'https'

    http = net_http_class(env).new(env[:url].host, env[:url].port || (is_ssl ? 443 : 80))
    if http.use_ssl = is_ssl
    ssl = env[:ssl]
    if ssl[:verify] == false
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    else
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE # <= PATCH or HACK ssl[:verify]
    end
    http.cert = ssl[:client_cert] if ssl[:client_cert]
    http.key = ssl[:client_key] if ssl[:client_key]
    http.ca_file = ssl[:ca_file] if ssl[:ca_file]
    end
    req = env[:request]
    http.read_timeout = net.open_timeout = req[:timeout] if req[:timeout]
    http.open_timeout = req[:open_timeout] if req[:open_timeout]

    full_path = full_path_for(env[:url].path, env[:url].query, env[:url].fragment)
    http_req = Net::HTTPGenericRequest.new(
    env[:method].to_s.upcase, # request method
    (env[:body] ? true : false), # is there data
    true, # does net/http love you, true or false?
    full_path, # request uri path
    env[:request_headers]) # request headers

    if env[:body].respond_to?(:read)
    http_req.body_stream = env[:body]
    env[:body] = nil
    end

    http_resp = http.request http_req, env[:body]

    resp_headers = {}
    http_resp.each_header do |key, value|
    resp_headers[key] = value
    end

    env.update \
    :status => http_resp.code.to_i,
    :response_headers => resp_headers,
    :body => http_resp.body

    @app.call env
    rescue Errno::ECONNREFUSED
    raise Error::ConnectionFailed.new(Errno::ECONNREFUSED)
    end

    def net_http_class(env)
    if proxy = env[:request][:proxy]
    Net::HTTP::Proxy(proxy[:uri].host, proxy[:uri].port, proxy[:user], proxy[:password])
    else
    Net::HTTP
    end
    end
    end
    end
    end

    ReplyDelete
  8. I'm using Faraday 0.6.1, and OAUTH2 (alone, not wrapped by anything). This was enough to solve the problem for me (on Gentoo, should work on Ubunto)

    Turn this

    client = OAuth2::Client.new(FACEBOOK_API_KEY, FACEBOOK_API_SECRET, :site => FACEBOOK_API_SITE)


    Into this

    client = OAuth2::Client.new(FACEBOOK_API_KEY, FACEBOOK_API_SECRET, :site => FACEBOOK_API_SITE, :ssl => {:ca_path => '/etc/ssl/certs' })

    ReplyDelete
  9. This seems to be a 1.9.x issue. Reverting to 1.8.7 fixed the issue.

    ReplyDelete
  10. I am on ubuntu 10.10 (Maverick)... struggled about 6 hours before I got it to work, sharing my experience

    1) did not try monkey patch
    2) tried {:client_options => {:ssl => {:ca_path => "/etc/ssl/certs"}} but still not worked
    3) tried ruby 1.8.7 still not worked
    4) tried different versions of omniauth & faraday, still no luck.

    The only thing that made it to work was following (thanks Alex)

    if Rails.env.development?
    OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
    end

    ReplyDelete
  11. My problem was solved by ensuring that openSSL was using the right certificate directory:

    For my system(ubuntu64) this was:
    ENV['SSL_CERT_DIR'] = '/usr/share/ca-certificates/'

    This was using jruby-openssl with JRuby 1.6.0

    I just added this setting to development.rb

    ReplyDelete
  12. Here's what I did that helped if you are specifically having a problem on Leopard.

    My cert was old and needed to be updated. I downloaded this:

    http://curl.haxx.se/ca/cacert.pem

    Then replaced my cert which was found here on Leopard:

    /usr/share/curl/curl-ca-bundle.crt

    Reload whatever you have that's accessing it and you should be good to go!

    ReplyDelete
  13. Just because instructions were a slight bit different for what worked for me, I thought I add my 2 cents:

    I'm on OS X Lion and using macports and rvm

    I installed curl-ca-bundle:

    sudo port install curl-ca-bundle


    Then I adjusted my omniauth config to be this:

    Rails.application.config.middleware.use OmniAuth::Builder do
    provider :google_oauth2, APP_CONFIG['CONSUMER_KEY'], APP_CONFIG['CONSUMER_SECRET'],
    :scope => 'https://www.google.com/m8/feeds https://www.googleapis.com/auth/userinfo.profile',
    :ssl => {:ca_path => "/share/curl/curl-ca-bundle.crt"}
    end

    ReplyDelete
  14. On Ubuntu, all I had to do was update /environments/development.rb to:

    Rails.application.config.middleware.use OmniAuth::Builder do
    provider :facebook, FACEBOOK_KEY, FACEBOOK_SECRET, {:client_options => {:ssl => {:ca_path => "/etc/ssl/certs"}}}
    end


    and then:

    cd /etc/ssl/certs
    sudo wget http://curl.haxx.se/ca/cacert.pem


    wola!

    ReplyDelete