OmniAuth & Facebook: 证书验证失败

66

我按照Railscast #235的指导尝试建立最简Facebook认证。

我首先设置了一个Twitter认证,就像Ryan自己所做的那样。这一步非常顺利。

然后我添加了Facebook登录。然而,在授权应用程序后,重定向到/auth/facebook/callback失败,并出现以下错误:

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

我正在本地工作。我没有在应用程序中设置任何SSL。我做错了什么?


5
这个1.9.2的问题有没有被“官方”解决的希望?我可能没有正确理解下面的答案,但是带有“PATCH或HACK”的if语句似乎是无用的(为什么要有if语句?),除了防止错误外,整个代码的目的没有任何解释。 - ClosureCowboy
@closurecowboy 如果你看一下Alex Kremer的回答,他详细解释了为什么会出现这个错误。实际上,在你的生产代码中,你应该避免使用被标记为答案的解决方案,因为使用VERIFY_NONE会禁用SSL证书对等检查。我已经将他的解决方案适应到了Omniauth的维基页面中。如果你或其他人有任何补充,请编辑它以改进。 - Eric Hu
所有Rails应用程序的通用解决方案在这里 https://dev59.com/B2gt5IYBdhLWcg3w8h01#16983443 - Pavel Nikolov
18个回答

74

实际问题是Faraday(Omniauth/Oauth用于HTTP调用)没有设置OpenSSL的ca_path变量。至少在Ubuntu上,大多数根证书存储在“/etc/ssl/certs”中。由于Faraday未设置此变量(并且当前没有方法可以这样做),OpenSSL找不到Facebook的SSL证书的根证书。

我已经向Faraday提交了一个拉请求,它将添加对此变量的支持,希望他们很快会采纳这个改变。在此之前,您可以使用猴子补丁将faraday更改为像这样或者使用我Fork的Faraday。之后,您应该在Gemspec中指定0.3.0版本的OAuth2 gem,以支持将SSL选项传递给Faraday。现在你只需要升级到Faraday 0.6.1,它支持传递ca_path变量,并升级到OmniAuth 0.2.2,它具有OAuth2的正确依赖项。然后,您就可以通过将以下内容添加到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

因此,简要概括一下:

  1. Faraday需要更新以支持SSL ca_path。安装Faraday 0.6.1
  2. 您的应用程序需要使用OAuth2版本0.3.0。由于当前在0.2.x树中有一个小版本依赖关系,您可能需要fork omniauth。升级到OmniAuth 0.2.2。
  3. 修改您的提供程序初始化器以指向您系统的证书路径(Ubuntu等上的“/etc/ssl/certs”)。

希望下一个Faraday和Omniauth的发布将合并此解决方案。

感谢KirylP为我指明正确的方向。


6
可以在Heroku上运行! - digitalWestie
7
最简单的方法可能是下载和安装它们 - 如果你是Mac用户,如果你已经安装了OpenSSL,那么你很可能已经拥有它们了。否则,如果只是为了开发,你可以添加一个初始化器来关闭对等验证:if Rails.env.development? OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE end - Alex Kremer
4
+1 @Alex Kremer 我使用了这个代码:Rails.application.config.middleware.use OmniAuth::Builder do :facebook, FACEBOOK_KEY, FACEBOOK_SECRET, {:client_options => {:ssl => {:ca_path => "/etc/ssl/certs"}}, :scope => 'publish_stream,email'} end 它起作用了,谢谢! - Surya
5
顺便问一句,有人知道在Windows中如何准确指定“cacerts.pem”路径吗?我已尝试过这两种方式:“C:\cacerts.pem”和“C:/cacerts.pem”(是的,文件确实存在),但运气不佳。仍然出现SSL错误。有人能帮忙吗? - dimitarvp
请看一下我在这里的回答:https://dev59.com/B2gt5IYBdhLWcg3w8h01#16983443 - Pavel Nikolov
显示剩余2条评论

18

我遇到了这个问题,尝试使用 :ca_path 参数解决,但没有成功。在 Github 上查找一段时间后,我发现有人建议使用 :ca_file 直接指向证书。

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

如果您需要获取系统证书文件的路径(并且您使用Linux),只需从终端输入以下命令。这将提供有关SSL设置的大量信息,包括路径(请参阅OPENSSLDIR)。您需要将certs/ca-bundle.crt添加到提供的路径中。

open-ssl version -a

非常感谢你,Emerson!我在Cent OS上遇到了这个问题。一些用于Google搜索的关键词:ca_file、Cent OS、Redhat、Fedora、pki、TLS、SSL。谢谢! - Sebastian Roth
1
这对我起了作用,在亚马逊 Linux 上 ca_path 没有作用。 - lawrence

14

我使用的是Ubuntu 10.10 (Maverick)...在花费了6个小时的努力后,终于让它工作了,现在分享我的经验。

  1. 没有尝试猴子补丁
  2. 尝试了 {:client_options => {:ssl => {:ca_path => "/etc/ssl/certs"}}但仍然无效
  3. 尝试了Ruby 1.8.7仍然无效
  4. 尝试不同版本的omniauth和faraday,仍然没有成功。

唯一能使其工作的是以下方法(感谢Alex)

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

我的 OS X 上也可以工作!谢谢!由于验证仅在开发环境中进行,因此在生产中不应构成安全风险。 - Ranhiru Jude Cooray
同样的事情!我选择这个解决方案,因为现有的解决方案都不适用于我 =( - PeaceDefener
禁用SSL验证是一个坏主意!不要这样做! - Dennis

7

成功通过 SSL 证书验证,就像应该做的那样。

我的项目正在使用 37signals ID 进行 Basecamp 集成(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

'CLIENT_ID'和'CLIENT_SECRET'可以在37signals.com获取,而证书捆绑文件gd_bundle.crt可从GoDaddy获取,因为37signals使用他们的CA。


6

如果你要部署到Heroku,你需要指定特定的文件位置。以下内容适用于我(在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

那你是怎么发现/想出来的? - Alex

5
我通过从http://certifie.com/ca-bundle/获取的CA捆绑包解决了这个问题。
在我的Devise初始化器中:
:client_options => { :ssl => { :ca_file => "#{Rails.root}/config/ca-bundle.crt" } } }

4

看起来Omniauth现在使用了更新的Faraday版本,这就解释了为什么上面的猴子补丁对我不起作用。我同意必须有更好的方法,但是对于任何其他只需要让它正常工作以进行测试的人,这里有一个更新的版本:

(在您的初始化程序目录中创建一个文件,并使用以下代码)

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

1
我也同意一定有更好的方法,但你的帖子对我很有帮助。我的代码之前使用的是Omniauth 0.2.0(使用Faraday 0.5.7而不是0.6.0),所以我只需在我的gemfile中加入gem 'omniauth 0.2.0'就可以让我先完成一些功能的开发。 - Eric Hu
1
我注意到这在 Faraday 0.6.1 上无法工作,请你更新一下代码,好吗? - Oded Harth
Randles出现问题,报错为“无方法错误:未定义方法response_headers' for #<Faraday::Adapter::NetHttp:0xb6d0e6dc>。您能否建议任何补充措施或需要进行的任何修改以使其正常工作? - Surya
看起来这个补丁已经过时了。我认为你应该查看Alex更新的答案来解决问题。 - Zac Randles
2
很抱歉,但是不行。任何涉及OpenSSL::SSL::VERIFY_NONE的答案都会极其不安全,不应该使用,即使它“能用”。 - Bob Aman
就像@BobAman所说的那样,你甚至不应该考虑那个选项! - jipiboily

4

2
我自己找到了这个解决方案,本来已经准备写在回答的最后了。我给它点了个赞。 - sufleR
1
这对我很有效,而且很简单。 - David DeMar

2

编辑:请查看下方的答案,因为它更相关

这个方法对我有效(修复来自https://github.com/jspooner):

在您的初始化目录中创建一个文件,并使用以下“猴子补丁”进行修补:

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

3
不适用于新版本的法拉第。请查看下面的答案... - Oded Harth
3
禁用SSL验证不是解决这个问题的好方法。请将OpenSSL指向正确的证书。 - eric

2
我正在使用Faraday 0.6.1和OAUTH2(单独使用,没有被任何东西包装)。这对我解决了问题(在Gentoo上,应该在Ubuntu上也适用)。
转换为:

我正在使用Faraday 0.6.1和OAUTH2(单独使用,没有被任何东西包装)。这对我解决了问题(在Gentoo上,应该在Ubuntu上也适用)。

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

进入此

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

这对我很有帮助。对于Oauth2 0.5.1,代码如下:oauth_client = OAuth2::Client.new( fb_app_id, fb_app_secret, { :site => 'https://graph.facebook.com', :token_url => '/oauth/access_token', :connection_opts => {:ssl => {:ca_path => "/etc/ssl/certs"}} }) - forresto

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接