Wednesday, April 2, 2014

Rails Omniauth OAuth 2 Faraday Getting Unparsable Binary Result from Google's userinfo Endpoint

Problem

My Rails 3 app's OAuth 2 integration was getting binary return values from Google's servers when accessing https://www.googleapis.com/oauth2/v1/userinfo. The below monkey patch, which can be put in a Rails initializer, solved the problem.

Solution

require 'omniauth/strategies/oauth2'

module OmniAuth
  module Strategies
    class GoogleOauth2 < OmniAuth::Strategies::OAuth2
      def raw_info
        return @raw_info if @raw_info
        @raw_info ||= access_token.get('https://www.googleapis.com/oauth2/v1/userinfo').parsed

        if !@raw_info.is_a?(Hash)
          # probably a parse error where we got a binary body...shame on faraday.
          s = RestClient.get("https://www.googleapis.com/oauth2/v1/userinfo?access_token=#{access_token.token}")
          @raw_info = MultiJson.decode(s)
        end
        @raw_info
      end
    end
  end
end

Discussion

Last week near the 26th, my Devise Omniauth Google OAuth2 logins were failing mysteriously.

The way this OAuth2 flow works, you get a 'code' from Google after the user tells it to authenticate with your application, your app then gets an access token with it, and in the middle of the specific Omniauth provider (in this case, the strategy OmniAuth::Strategies::GoogleOauth2), inside of the raw_info method, it makes a call to Google using the Access Token it just got from the code.

This call is performed by Faraday, an HTTP client that can wrap Net::HTTP. Google was responding to it in a binary format. Faraday tries to parse the result with JSON, and if it fails, it fails silently with a rescue and a return of the raw response body that it just failed to parse.

However, I found that if I used the access token in a plain web browser, I could see the expected JSON result just fine.

Why would it work in my web browser but not from Faraday?

I don't know, but I inserted the above monkey patch. If parsing the result from Google fails, it fires off an additional call with RestClient and the access token as a GET parameter - a perfectly valid OAuth2 call. Stuff like this works with Facebook's OAuth (2?) integration, and their Graph API, as well, I think.

The login failures were not always happening. That the above solved the problem in development mode, locally, is evidence that something weird was going on between Devise, Omniauth, Omniauth OAuth2, Faraday, Net::HTTP, or on Google's end.

As an aside, this is one of the most ridiculous bug work arounds I've ever resorted to.