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