Codebase list ruby-omniauth-facebook / 260cb64 lib / omniauth / strategies / facebook.rb
260cb64

Tree @260cb64 (Download .tar.gz)

facebook.rb @260cb64raw · history · blame

require 'omniauth/strategies/oauth2'
require 'base64'
require 'openssl'
require 'rack/utils'

module OmniAuth
  module Strategies
    class Facebook < OmniAuth::Strategies::OAuth2
      class NoAuthorizationCodeError < StandardError; end

      DEFAULT_SCOPE = 'email'

      option :client_options, {
        :site => 'https://graph.facebook.com',
        :token_url => '/oauth/access_token'
      }

      option :token_params, {
        :parse => :query
      }

      option :access_token_options, {
        :header_format => 'OAuth %s',
        :param_name => 'access_token'
      }

      option :authorize_options, [:scope, :display, :auth_type]

      uid { raw_info['id'] }

      info do
        prune!({
          'nickname' => raw_info['username'],
          'email' => raw_info['email'],
          'name' => raw_info['name'],
          'first_name' => raw_info['first_name'],
          'last_name' => raw_info['last_name'],
          'image' => "#{options[:secure_image_url] ? 'https' : 'http'}://graph.facebook.com/#{uid}/picture?type=#{options[:image_size] || 'square'}",
          'description' => raw_info['bio'],
          'urls' => {
            'Facebook' => raw_info['link'],
            'Website' => raw_info['website']
          },
          'location' => (raw_info['location'] || {})['name'],
          'verified' => raw_info['verified']
        })
      end

      extra do
        hash = {}
        hash['raw_info'] = raw_info unless skip_info?
        prune! hash
      end

      def raw_info
        @raw_info ||= access_token.get('/me').parsed || {}
      end

      def build_access_token
        if access_token = request.params["access_token"]
          ::OAuth2::AccessToken.from_hash(
            client, 
            {"access_token" => access_token}.update(access_token_options)
          )
        elsif signed_request_contains_access_token?
          hash = signed_request.clone
          ::OAuth2::AccessToken.new(
            client,
            hash.delete('oauth_token'),
            hash.merge!(access_token_options.merge(:expires_at => hash.delete('expires')))
          )
        else
          with_authorization_code! { super }.tap do |token|
            token.options.merge!(access_token_options)
          end
        end
      end

      def request_phase
        if signed_request_contains_access_token?
          # if we already have an access token, we can just hit the
          # callback URL directly and pass the signed request along
          params = { :signed_request => raw_signed_request }
          params[:state] = request.params['state'] if request.params['state']
          query = Rack::Utils.build_query(params)

          url = callback_url
          url << "?" unless url.match(/\?/)
          url << "&" unless url.match(/[\&\?]$/)
          url << query

          redirect url
        else
          super
        end
      end

      # NOTE if we're using code from the signed request
      # then FB sets the redirect_uri to '' during the authorize
      # phase + it must match during the access_token phase:
      # https://github.com/facebook/php-sdk/blob/master/src/base_facebook.php#L348
      def callback_url
        if @authorization_code_from_signed_request
          ''
        else
          options[:callback_url] || super
        end
      end

      def access_token_options
        options.access_token_options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
      end

      ##
      # You can pass +display+, +state+ or +scope+ params to the auth request, if
      # you need to set them dynamically. You can also set these options
      # in the OmniAuth config :authorize_params option.
      #
      # /auth/facebook?display=popup&state=ABC
      #
      def authorize_params
        super.tap do |params|
          %w[display state scope].each do |v|
            if request.params[v]
              params[v.to_sym] = request.params[v]

              # to support omniauth-oauth2's auto csrf protection
              session['omniauth.state'] = params[:state] if v == 'state'
            end
          end

          params[:scope] ||= DEFAULT_SCOPE
        end
      end

      ##
      # Parse signed request in order, from:
      #
      # 1. the request 'signed_request' param (server-side flow from canvas pages) or
      # 2. a cookie (client-side flow via JS SDK)
      #
      def signed_request
        @signed_request ||= raw_signed_request &&
          parse_signed_request(raw_signed_request)
      end

      private

      def raw_signed_request
        request.params['signed_request'] ||
        request.cookies["fbsr_#{client.id}"]
      end

      ##
      # If the signed_request comes from a FB canvas page and the user
      # has already authorized your application, the JSON object will be
      # contain the access token.
      #
      # https://developers.facebook.com/docs/authentication/canvas/
      #
      def signed_request_contains_access_token?
        signed_request &&
        signed_request['oauth_token']
      end

      ##
      # Picks the authorization code in order, from:
      #
      # 1. the request 'code' param (manual callback from standard server-side flow)
      # 2. a signed request (see #signed_request for more)
      #
      def with_authorization_code!
        if request.params.key?('code')
          yield
        elsif code_from_signed_request = signed_request && signed_request['code']
          request.params['code'] = code_from_signed_request
          @authorization_code_from_signed_request = true
          begin
            yield
          ensure
            request.params.delete('code')
            @authorization_code_from_signed_request = false
          end
        else
          raise NoAuthorizationCodeError, 'must pass either a `code` parameter or a signed request (via `signed_request` parameter or a `fbsr_XXX` cookie)'
        end
      end

      def prune!(hash)
        hash.delete_if do |_, value|
          prune!(value) if value.is_a?(Hash)
          value.nil? || (value.respond_to?(:empty?) && value.empty?)
        end
      end

      def parse_signed_request(value)
        signature, encoded_payload = value.split('.')

        decoded_hex_signature = base64_decode_url(signature)
        decoded_payload = MultiJson.decode(base64_decode_url(encoded_payload))

        unless decoded_payload['algorithm'] == 'HMAC-SHA256'
          raise NotImplementedError, "unkown algorithm: #{decoded_payload['algorithm']}"
        end

        if valid_signature?(client.secret, decoded_hex_signature, encoded_payload)
          decoded_payload
        end
      end

      def valid_signature?(secret, signature, payload, algorithm = OpenSSL::Digest::SHA256.new)
        OpenSSL::HMAC.digest(algorithm, secret, payload) == signature
      end

      def base64_decode_url(value)
        value += '=' * (4 - value.size.modulo(4))
        Base64.decode64(value.tr('-_', '+/'))
      end
    end
  end
end