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