Search code examples
facebookauthenticationrubygemssinatraoauth-2.0

Sinatra Logging in to Facebook OAuth 2.0 using gem oauth2


I can navigate to the main url which successfuly redirects to facebook. I grant permission, and I am redirected back to the callback url. If this url simply returns something like 'hello', it works fine no errors. But calling token = client.auth_code.get_token(@data[:code], :redirect_uri => redirect_uri) causes the error.


Solution

  • Ok finally got this to work. The error being reported was just some weird thing with error handling and had nothing to do with the actual problem. The problem was that the oauth2 gem is generic and you have to taylor a few things to make it work with facebook. These are the things you have to do that differ from the readme (see issues 70 and 75 on github for more info)

    Before you create your client, you must register a parser for the facebook response:

    OAuth2::Response.register_parser(:facebook, 'text/plain') do |body|
            token_key, token_value, expiration_key, expiration_value = body.split(/[=&]/)
            {token_key => token_value, expiration_key => expiration_value, :mode => :query, :param_name => 'access_token'}
    end
    

    You also have to set the token url for the client on creation:

      @client = OAuth2::Client.new(ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'], {:site => 'https://graph.facebook.com', :token_url => '/oauth/access_token'})
    

    When the oauth receives a response, it uses the parser you tell it to use to parse the response into a hash. The custom :facebook parser ensures that the hash contains the access token and expires string and tells it to use a mode of query and that the param name is access_token. Without the mode and param_name the Oauth client would attempt to send the access token in a header rather than a query string when accessing resources. Facebook expects the access token to be in the url. Without param_name, the oauth client sends it as https://graph.facebook.com/bearer_token=ABC. With the param_name, it is https://graph.facebook.com/access_token=ABC

    Finally when you create your AccessToken object, be sure to tell it to use your custom parser like so:

    token = client.auth_code.get_token(@data[:code], {:redirect_uri => redirect_uri, :parsed => :facebook})
    

    Altogether it looks like:

    require 'sinatra'
    require 'oauth2'
    require 'json'
    class App < Sinatra::Base
    
      configure do
        set :views_folder,  File.join($BP, 'views')
        set :public_folder, File.join($BP, 'public')
      end
    
      before do
        @data = JSON.parse(request.env["rack.input"].read) if request.request_method =~ /POST|PUT|DELETE/i
        @data = params if request.request_method == 'GET'
      end
    
      before do
        pass if (request.path_info == '/auth/facebook' || request.path_info == '/auth/facebook/callback')
        redirect to('/auth/facebook') unless self.logged_in
      end
    
      get "/" do
        request.request_method
      end
    
      def client
        if !@client
          OAuth2::Response.register_parser(:facebook, 'text/plain') do |body|
            token_key, token_value, expiration_key, expiration_value = body.split(/[=&]/)
            {token_key => token_value, expiration_key => expiration_value, :mode => :query, :param_name => 'access_token'}
          end
          @client = OAuth2::Client.new(ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'], {:site => 'https://graph.facebook.com', :token_url => '/oauth/access_token'})
        end
        @client
      end
    
      get '/auth/facebook' do
        redirect client.auth_code.authorize_url(
          :redirect_uri => redirect_uri,
          :scope => 'email'
        )
      end
    
      get '/auth/facebook/callback' do
        token = client.auth_code.get_token(@data[:code], {:redirect_uri => redirect_uri, :parsed => :facebook})
        user = token.get('/me').parsed
        create_user user unless user_exists user
      end
    
      def redirect_uri
        uri = URI.parse(request.url)
        uri.path = '/auth/facebook/callback'
        uri.query = nil
        uri.to_s
      end
    end