Codebase list ruby-omniauth-facebook / debian/1.6.0-2 lib / omniauth / strategies / facebook.rb
debian/1.6.0-2

Tree @debian/1.6.0-2 (Download .tar.gz)

facebook.rb @debian/1.6.0-2

260cb64
 
 
 
4d6f3f6
260cb64
 
 
 
 
4d6f3f6
260cb64
 
 
 
 
4d6f3f6
260cb64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d6f3f6
260cb64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d6f3f6
260cb64
 
4d6f3f6
 
 
 
 
 
 
 
 
 
 
 
 
 
260cb64
 
 
 
4d6f3f6
260cb64
 
 
 
 
 
 
 
 
 
 
 
 
 
4d6f3f6
 
 
260cb64
 
 
 
 
 
 
 
 
 
 
 
4d6f3f6
 
260cb64
4d6f3f6
260cb64
 
4d6f3f6
260cb64
 
 
 
 
 
 
 
 
 
 
4d6f3f6
 
260cb64
4d6f3f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260cb64
 
 
 
 
4d6f3f6
260cb64
 
4d6f3f6
 
260cb64
 
 
4d6f3f6
260cb64
 
 
 
4d6f3f6
 
260cb64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d6f3f6
260cb64
 
 
 
 
4d6f3f6
260cb64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d6f3f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260cb64
 
 
require 'omniauth/strategies/oauth2'
require 'base64'
require 'openssl'
require 'rack/utils'
require 'uri'

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

      DEFAULT_SCOPE = 'email'

      option :client_options, {
        :site => 'https://graph.facebook.com',
        :authorize_url => "https://www.facebook.com/dialog/oauth",
        :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' => image_url(uid, options),
          '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', info_options).parsed || {}
      end

      def info_options
        params = {:appsecret_proof => appsecret_proof}
        params.merge!({:fields => options[:info_fields]}) if options[:info_fields]
        params.merge!({:locale => options[:locale]}) if options[:locale]

        { :params => params }
      end

      def callback_phase
        super
      rescue NoAuthorizationCodeError => e
        fail!(:no_authorization_code, e)
      rescue UnknownSignatureAlgorithmError => e
        fail!(:unknown_signature_algoruthm, e)
      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.
          params = { :signed_request => raw_signed_request }
          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 and 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+, +scope+, or +auth_type+ 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
      def authorize_params
        super.tap do |params|
          %w[display scope auth_type].each do |v|
            if request.params[v]
              params[v.to_sym] = request.params[v]
            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

      protected

      def build_access_token
        if 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

      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('.')
        return if signature.nil?

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

        unless decoded_payload['algorithm'] == 'HMAC-SHA256'
          raise UnknownSignatureAlgorithmError, "unknown 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

      def image_url(uid, options)
        uri_class = options[:secure_image_url] ? URI::HTTPS : URI::HTTP
        url = uri_class.build({:host => 'graph.facebook.com', :path => "/#{uid}/picture"})

        query = if options[:image_size].is_a?(String)
          { :type => options[:image_size] }
        elsif options[:image_size].is_a?(Hash)
          options[:image_size]
        end
        url.query = Rack::Utils.build_query(query) if query

        url.to_s
      end

      def appsecret_proof
        @appsecret_proof ||= OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, client.secret, access_token.token)
      end
    end
  end
end