├── lib ├── omniauth-facebook.rb └── omniauth │ ├── facebook.rb │ ├── facebook │ ├── version.rb │ └── signed_request.rb │ └── strategies │ └── facebook.rb ├── Gemfile ├── .gitignore ├── example ├── Gemfile ├── config.ru ├── Gemfile.lock └── app.rb ├── Rakefile ├── test ├── fixtures │ ├── payload.json │ └── signed_request.txt ├── signed_request_test.rb ├── helper.rb ├── support │ └── shared_examples.rb └── strategy_test.rb ├── .github └── workflows │ ├── ci.yml │ └── stale.yml ├── omniauth-facebook.gemspec ├── CHANGELOG.md └── README.md /lib/omniauth-facebook.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth/facebook' 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rack', '>= 2.0' 6 | -------------------------------------------------------------------------------- /lib/omniauth/facebook.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth/facebook/version' 2 | require 'omniauth/strategies/facebook' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .rspec 4 | /Gemfile.lock 5 | pkg/* 6 | .powenv 7 | .powder 8 | tmp 9 | bin 10 | -------------------------------------------------------------------------------- /lib/omniauth/facebook/version.rb: -------------------------------------------------------------------------------- 1 | module OmniAuth 2 | module Facebook 3 | VERSION = '10.0.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra' 4 | gem 'sinatra-reloader' 5 | gem 'omniauth-facebook', path: '../' 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new do |task| 5 | task.libs << 'test' 6 | task.test_files = FileList['test/*_test.rb'] 7 | end 8 | 9 | task default: :test 10 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'omniauth-facebook' 3 | require './app.rb' 4 | 5 | use Rack::Session::Cookie, secret: 'rqt2iy17g0vpkouu995r598671cihpae9mritav0yctevwqhprpr71oumzlv5c3z' 6 | 7 | use OmniAuth::Builder do 8 | provider :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'] 9 | end 10 | 11 | run Sinatra::Application 12 | -------------------------------------------------------------------------------- /test/fixtures/payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "algorithm": "HMAC-SHA256", 3 | "expires": 1308988800, 4 | "issued_at": 1308985018, 5 | "oauth_token": "111111111111111|2.AQBAttRlLVnwqNPZ.3600.1111111111.1-111111111111111|T49w3BqoZUegypru51Gra70hED8", 6 | "user": 7 | { 8 | "country": "de", 9 | "locale": "en_US", 10 | "age": 11 | { 12 | "min": 21 13 | } 14 | }, 15 | "user_id": "111111111111111" 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/signed_request.txt: -------------------------------------------------------------------------------- 1 | 53umfudisP7mKhsi9nZboBg15yMZKhfQAARL9UoZtSE.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEzMDg5ODg4MDAsImlzc3VlZF9hdCI6MTMwODk4NTAxOCwib2F1dGhfdG9rZW4iOiIxMTExMTExMTExMTExMTF8Mi5BUUJBdHRSbExWbndxTlBaLjM2MDAuMTExMTExMTExMS4xLTExMTExMTExMTExMTExMXxUNDl3M0Jxb1pVZWd5cHJ1NTFHcmE3MGhFRDgiLCJ1c2VyIjp7ImNvdW50cnkiOiJkZSIsImxvY2FsZSI6ImVuX1VTIiwiYWdlIjp7Im1pbiI6MjF9fSwidXNlcl9pZCI6IjExMTExMTExMTExMTExMSJ9 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: 12 | - "3.0" 13 | - "3.1" 14 | - "3.2" 15 | - "3.3" 16 | - head 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby }} 23 | bundler-cache: true # 'bundle install' and cache 24 | - name: Run tests 25 | run: bundle exec rake 26 | -------------------------------------------------------------------------------- /test/signed_request_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require 'omniauth/facebook/signed_request' 3 | 4 | class SignedRequestTest < Minitest::Test 5 | def setup 6 | @value = fixture('signed_request.txt').strip 7 | @secret = "897z956a2z7zzzzz5783z458zz3z7556" 8 | @expected_payload = JSON.parse(fixture('payload.json')) 9 | end 10 | 11 | def test_signed_request_payload 12 | signed_request = OmniAuth::Facebook::SignedRequest.new(@value, @secret) 13 | assert_equal @expected_payload, signed_request.payload 14 | end 15 | 16 | def test_signed_request_parse 17 | payload = OmniAuth::Facebook::SignedRequest.parse(@value, @secret) 18 | assert_equal @expected_payload, payload 19 | end 20 | 21 | private 22 | 23 | def fixture(name) 24 | File.read(File.expand_path("fixtures/#{name}", File.dirname(__FILE__))) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 17 | stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 18 | stale-issue-label: 'no-issue-activity' 19 | stale-pr-label: 'no-pr-activity' 20 | days-before-stale: 90 21 | days-before-close: 60 22 | exempt-pr-label: 'pinned' 23 | exempt-issue-label: 'pinned' 24 | -------------------------------------------------------------------------------- /omniauth-facebook.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'omniauth/facebook/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'omniauth-facebook' 7 | s.version = OmniAuth::Facebook::VERSION 8 | s.authors = ['Mark Dodwell', 'Josef Šimánek'] 9 | s.email = ['mark@madeofcode.com', 'retro@ballgag.cz'] 10 | s.summary = 'Facebook OAuth2 Strategy for OmniAuth' 11 | s.homepage = 'https://github.com/simi/omniauth-facebook' 12 | s.license = 'MIT' 13 | 14 | s.files = `git ls-files`.split("\n").reject { |path| path.start_with? "example/" } 15 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 16 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 17 | s.require_paths = ['lib'] 18 | 19 | s.add_runtime_dependency 'omniauth-oauth2', '>= 1.2', '< 3' 20 | s.add_runtime_dependency 'bigdecimal' 21 | 22 | s.add_development_dependency 'minitest' 23 | s.add_development_dependency 'mocha' 24 | s.add_development_dependency 'rake' 25 | end 26 | -------------------------------------------------------------------------------- /lib/omniauth/facebook/signed_request.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module OmniAuth 4 | module Facebook 5 | class SignedRequest 6 | class UnknownSignatureAlgorithmError < NotImplementedError; end 7 | SUPPORTED_ALGORITHM = 'HMAC-SHA256' 8 | 9 | attr_reader :value, :secret 10 | 11 | def self.parse(value, secret) 12 | new(value, secret).payload 13 | end 14 | 15 | def initialize(value, secret) 16 | @value = value 17 | @secret = secret 18 | end 19 | 20 | def payload 21 | @payload ||= parse_signed_request 22 | end 23 | 24 | private 25 | 26 | def parse_signed_request 27 | signature, encoded_payload = value.split('.') 28 | return if signature.nil? 29 | 30 | decoded_hex_signature = base64_decode_url(signature) 31 | decoded_payload = JSON.parse(base64_decode_url(encoded_payload)) 32 | 33 | unless decoded_payload['algorithm'] == SUPPORTED_ALGORITHM 34 | raise UnknownSignatureAlgorithmError, "unknown algorithm: #{decoded_payload['algorithm']}" 35 | end 36 | 37 | if valid_signature?(decoded_hex_signature, encoded_payload) 38 | decoded_payload 39 | end 40 | end 41 | 42 | def valid_signature?(signature, payload, algorithm = OpenSSL::Digest::SHA256.new) 43 | OpenSSL::HMAC.digest(algorithm, secret, payload) == signature 44 | end 45 | 46 | def base64_decode_url(value) 47 | value += '=' * (4 - value.size.modulo(4)) 48 | Base64.decode64(value.tr('-_', '+/')) 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'minitest/autorun' 3 | require 'mocha/minitest' 4 | require 'omniauth/strategies/facebook' 5 | 6 | OmniAuth.config.test_mode = true 7 | 8 | module BlockTestHelper 9 | def test(name, &blk) 10 | method_name = "test_#{name.gsub(/\s+/, '_')}" 11 | raise "Method already defined: #{method_name}" if instance_methods.include?(method_name.to_sym) 12 | define_method method_name, &blk 13 | end 14 | end 15 | 16 | module CustomAssertions 17 | def assert_has_key(key, hash, msg = nil) 18 | msg = message(msg) { "Expected #{hash.inspect} to have key #{key.inspect}" } 19 | assert hash.has_key?(key), msg 20 | end 21 | 22 | def refute_has_key(key, hash, msg = nil) 23 | msg = message(msg) { "Expected #{hash.inspect} not to have key #{key.inspect}" } 24 | refute hash.has_key?(key), msg 25 | end 26 | end 27 | 28 | class TestCase < Minitest::Test 29 | extend BlockTestHelper 30 | include CustomAssertions 31 | end 32 | 33 | class StrategyTestCase < TestCase 34 | def setup 35 | @request = stub('Request') 36 | @request.stubs(:params).returns({}) 37 | @request.stubs(:cookies).returns({}) 38 | @request.stubs(:env).returns({}) 39 | @request.stubs(:scheme).returns({}) 40 | @request.stubs(:ssl?).returns(false) 41 | 42 | @client_id = '123' 43 | @client_secret = '53cr3tz' 44 | @options = {} 45 | 46 | @facebook_api_version = OmniAuth::Strategies::Facebook::DEFAULT_FACEBOOK_API_VERSION 47 | end 48 | 49 | def strategy 50 | @strategy ||= begin 51 | args = [@client_id, @client_secret, @options].compact 52 | OmniAuth::Strategies::Facebook.new(nil, *args).tap do |strategy| 53 | strategy.stubs(:request).returns(@request) 54 | end 55 | end 56 | end 57 | end 58 | 59 | Dir[File.expand_path('../support/**/*', __FILE__)].each(&method(:require)) 60 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | omniauth-facebook (10.0.0) 5 | bigdecimal 6 | omniauth-oauth2 (>= 1.2, < 3) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | base64 (0.2.0) 12 | bigdecimal (3.1.8) 13 | faraday (2.12.0) 14 | faraday-net_http (>= 2.0, < 3.4) 15 | json 16 | logger 17 | faraday-net_http (3.3.0) 18 | net-http 19 | hashie (5.0.0) 20 | json (2.7.2) 21 | jwt (2.9.3) 22 | base64 23 | logger (1.6.1) 24 | multi_json (1.15.0) 25 | multi_xml (0.7.1) 26 | bigdecimal (~> 3.1) 27 | mustermann (3.0.3) 28 | ruby2_keywords (~> 0.0.1) 29 | net-http (0.4.1) 30 | uri 31 | oauth2 (2.0.9) 32 | faraday (>= 0.17.3, < 3.0) 33 | jwt (>= 1.0, < 3.0) 34 | multi_xml (~> 0.5) 35 | rack (>= 1.2, < 4) 36 | snaky_hash (~> 2.0) 37 | version_gem (~> 1.1) 38 | omniauth (2.1.2) 39 | hashie (>= 3.4.6) 40 | rack (>= 2.2.3) 41 | rack-protection 42 | omniauth-oauth2 (1.8.0) 43 | oauth2 (>= 1.4, < 3) 44 | omniauth (~> 2.0) 45 | rack (3.1.8) 46 | rack-protection (4.0.0) 47 | base64 (>= 0.1.0) 48 | rack (>= 3.0.0, < 4) 49 | rack-session (2.0.0) 50 | rack (>= 3.0.0) 51 | ruby2_keywords (0.0.5) 52 | sinatra (4.0.0) 53 | mustermann (~> 3.0) 54 | rack (>= 3.0.0, < 4) 55 | rack-protection (= 4.0.0) 56 | rack-session (>= 2.0.0, < 3) 57 | tilt (~> 2.0) 58 | sinatra-contrib (4.0.0) 59 | multi_json (>= 0.0.2) 60 | mustermann (~> 3.0) 61 | rack-protection (= 4.0.0) 62 | sinatra (= 4.0.0) 63 | tilt (~> 2.0) 64 | sinatra-reloader (1.0) 65 | sinatra-contrib 66 | snaky_hash (2.0.1) 67 | hashie 68 | version_gem (~> 1.1, >= 1.1.1) 69 | tilt (2.4.0) 70 | uri (0.13.1) 71 | version_gem (1.1.4) 72 | 73 | PLATFORMS 74 | ruby 75 | x64-mingw32 76 | 77 | DEPENDENCIES 78 | omniauth-facebook! 79 | sinatra 80 | sinatra-reloader 81 | 82 | BUNDLED WITH 83 | 2.5.14 84 | -------------------------------------------------------------------------------- /example/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require "sinatra/reloader" 3 | require 'yaml' 4 | require 'json' 5 | 6 | # configure sinatra 7 | set :run, false 8 | set :raise_errors, true 9 | 10 | # REQUEST STEP (server-side flow) 11 | get '/server-side' do 12 | # NOTE: You would just hit this endpoint directly from the browser in a real app. The redirect is 13 | # just here to explicit declare this server-side flow. 14 | redirect '/auth/facebook' 15 | end 16 | 17 | # REQUEST STEP (client-side flow) 18 | get '/client-side' do 19 | content_type 'text/html' 20 | # NOTE: When you enable cookie below in the FB.init call the GET request in the FB.login callback 21 | # will send a signed request in a cookie back the OmniAuth callback which will parse out the 22 | # authorization code and obtain an access_token with it. 23 | <<-HTML 24 | 25 | 26 | Client-side Flow Example 27 | 28 | 46 | 47 | 48 |
49 | 50 |

51 | Connect to FB! 52 |

53 | 54 |

55 | 56 | 75 | 76 | 77 | HTML 78 | end 79 | 80 | # CALLBACK STEP 81 | # - redirected here for server-side flow 82 | # - ajax request made here for client-side flow 83 | get '/auth/:provider/callback' do 84 | content_type 'application/json' 85 | JSON.dump(request.env) 86 | end 87 | -------------------------------------------------------------------------------- /test/support/shared_examples.rb: -------------------------------------------------------------------------------- 1 | # NOTE it would be useful if this lived in omniauth-oauth2 eventually 2 | module OAuth2StrategyTests 3 | def self.included(base) 4 | base.class_eval do 5 | include ClientTests 6 | include AuthorizeParamsTests 7 | include CSRFAuthorizeParamsTests 8 | include TokenParamsTests 9 | end 10 | end 11 | 12 | module ClientTests 13 | extend BlockTestHelper 14 | 15 | test 'should be initialized with symbolized client_options' do 16 | @options = { client_options: { 'authorize_url' => 'https://example.com' } } 17 | assert_equal 'https://example.com', strategy.client.options[:authorize_url] 18 | end 19 | end 20 | 21 | module AuthorizeParamsTests 22 | extend BlockTestHelper 23 | 24 | test 'should include any authorize params passed in the :authorize_params option' do 25 | @options = { authorize_params: { foo: 'bar', baz: 'zip' } } 26 | assert_equal 'bar', strategy.authorize_params['foo'] 27 | assert_equal 'zip', strategy.authorize_params['baz'] 28 | end 29 | 30 | test 'should include top-level options that are marked as :authorize_options' do 31 | @options = { authorize_options: [:scope, :foo], scope: 'bar', foo: 'baz' } 32 | assert_equal 'bar', strategy.authorize_params['scope'] 33 | assert_equal 'baz', strategy.authorize_params['foo'] 34 | end 35 | 36 | test 'should exclude top-level options that are not passed' do 37 | @options = { authorize_options: [:bar] } 38 | refute_has_key :bar, strategy.authorize_params 39 | refute_has_key 'bar', strategy.authorize_params 40 | end 41 | end 42 | 43 | module CSRFAuthorizeParamsTests 44 | extend BlockTestHelper 45 | 46 | test 'should store random state in the session when none is present in authorize or request params' do 47 | assert_includes strategy.authorize_params.keys, 'state' 48 | refute_empty strategy.authorize_params['state'] 49 | refute_empty strategy.session['omniauth.state'] 50 | assert_equal strategy.authorize_params['state'], strategy.session['omniauth.state'] 51 | end 52 | 53 | test 'should not store state in the session when present in authorize params vs. a random one' do 54 | @options = { authorize_params: { state: 'bar' } } 55 | refute_empty strategy.authorize_params['state'] 56 | refute_equal 'bar', strategy.authorize_params[:state] 57 | refute_empty strategy.session['omniauth.state'] 58 | refute_equal 'bar', strategy.session['omniauth.state'] 59 | end 60 | 61 | test 'should not store state in the session when present in request params vs. a random one' do 62 | @request.stubs(:params).returns({ 'state' => 'foo' }) 63 | refute_empty strategy.authorize_params['state'] 64 | refute_equal 'foo', strategy.authorize_params[:state] 65 | refute_empty strategy.session['omniauth.state'] 66 | refute_equal 'foo', strategy.session['omniauth.state'] 67 | end 68 | end 69 | 70 | module TokenParamsTests 71 | extend BlockTestHelper 72 | 73 | test 'should include any authorize params passed in the :token_params option' do 74 | @options = { token_params: { foo: 'bar', baz: 'zip' } } 75 | assert_equal 'bar', strategy.token_params['foo'] 76 | assert_equal 'zip', strategy.token_params['baz'] 77 | end 78 | 79 | test 'should include top-level options that are marked as :token_options' do 80 | @options = { token_options: [:scope, :foo], scope: 'bar', foo: 'baz' } 81 | assert_equal 'bar', strategy.token_params['scope'] 82 | assert_equal 'baz', strategy.token_params['foo'] 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 10.0.0 (2024-05-23) 2 | 3 | Changes: 4 | 5 | - bumped version of FB Graph API to v19.0 6 | 7 | ## 9.0.0 (2021-10-25) 8 | 9 | Changes: 10 | 11 | - bumped version of FB Graph API to v5.0 12 | 13 | ## 8.0.0 (2020-10-20) 14 | 15 | Changes: 16 | 17 | - user profile picture link includes access token (#344, @anklos) 18 | 19 | ## 7.0.0 (2020-08-03) 20 | 21 | Changes: 22 | 23 | - bumped version of FB Graph API to v4.0 24 | 25 | ## 6.0.0 (2020-01-27) 26 | 27 | Changes: 28 | 29 | - bumped version of FB Graph API to v3.0 30 | 31 | ## 5.0.0 (2018-03-29) 32 | 33 | Changes: 34 | 35 | - bumped version of FB Graph API to v2.10 (#297, @piotrjaworski) 36 | - use only CRuby 2.0+ on CI (#298, @simi) 37 | 38 | ## 4.0.0 (2016-07-26) 39 | 40 | Changes: 41 | 42 | - drop support for Ruby < 1.9.3 (@mkdynamic) 43 | - switch to versioned FB APIs, currently using v2.6 (#245, @printercu, @mkdynamic) 44 | - remove deprecated :nickname field from README example (#223, @abelorian) 45 | - add Ruby 2.2 + 2.3.0 to CI (#225, @tricknotes, @mkdynamic, @anoraak) 46 | - update example app (@mkdynamic) 47 | 48 | ## 3.0.0 (2015-10-26) 49 | 50 | Changes: 51 | 52 | - remove query string from redirect_uri on callback by default (#221, @gioblu) 53 | - signed request parsing extracted to `OmniAuth::Facebook::SignedRequest` class. (#183, @simi, @Vrael) 54 | - change default value of `info_fields` to `name,email` for the [graph-api-v2.4](https://developers.facebook.com/blog/post/2015/07/08/graph-api-v2.4/). ([#209](https://github.com/mkdynamic/omniauth-facebook/pull/209)) 55 | 56 | ## 2.0.1 (2015-02-21) 57 | 58 | Bugfixes: 59 | 60 | - allow versioning by not forcing absolute path for graph requests (#180, @frausto) 61 | - allow the image_size option to be set as a symbol. (#182, @jgrau) 62 | 63 | ## 2.0.0 (2014-08-07) 64 | 65 | Changes: 66 | 67 | - remove support for canvas app flow (765ed9, @mkdynamic) 68 | 69 | Bugfixes: 70 | 71 | - bump omniauth-oauth2 dependency which addresses CVE-2012-6134 (#162, @linedotstar) 72 | - rescue `NoAuthorizationCodeError` in callback_phase (a0036b, @tomoya55) 73 | - fix CSRF exception when using FB JS SDK and parsing signed request (765ed9, @mkdynamic) 74 | 75 | ## 1.6.0 (2014-01-13) 76 | 77 | Features: 78 | 79 | - ability to specify `auth_type` per-request (#78, @sebastian-stylesaint) 80 | - image dimension can be set using `image_size` option (#91, @weilu) 81 | - update Facebook authorize URL to fix broken authorization (#103, @dlackty) 82 | - adds `info_fields` option (#109, @bloudermilk) 83 | - adds `locale` parameter (#133, @donbobka, @simi) 84 | - add automatically `appsecret_proof` (#140, @nlsrchtr, @simi) 85 | 86 | Changes: 87 | 88 | - `NoAuthorizationCodeError` and `UnknownSignatureAlgorithmError` will now `fail!` (#117, @nchelluri) 89 | - don't try to parse the signature if it's nil (#127, @oriolgual) 90 | 91 | ## 1.5.1 (2013-11-18) 92 | 93 | Changes: 94 | 95 | - don't use `access_token` in URL [CVE-2013-4593](https://github.com/mkdynamic/omniauth-facebook/wiki/Access-token-vulnerability:-CVE-2013-4593) (@homakov, @mkdynamic, @simi) 96 | 97 | ## 1.5.0 (2013-11-13) 98 | 99 | Changes: 100 | 101 | - remove `state` param to fix CSRF vulnerabilty [CVE-2013-4562](https://github.com/mkdynamic/omniauth-facebook/wiki/CSRF-vulnerability:-CVE-2013-4562) (@homakov, @mkdynamic, @simi) 102 | 103 | ## 1.4.1 (2012-07-07) 104 | 105 | Changes: 106 | 107 | - update to omniauth-oauth2 1.1.0 for csrf protection (@mkdynamic) 108 | 109 | ## 1.4.0 (2012-06-24) 110 | 111 | Features: 112 | 113 | - obey `skip_info?` config (@mkdynamic) 114 | - add support of the `:auth_type` option to `:authorize_options` (#58, @JHeidinga, @mkdynamic) 115 | - support `access_token` parameter as part of the callback request (#62, @steverandy) 116 | 117 | ## 1.3.0 (2012-05-05) 118 | 119 | Features: 120 | 121 | - dynamic permissions in the auth params (#30, @famoseagle) 122 | - add support for facebook canvas (@mkdynamic) 123 | - add verified key to the info hash (#34, @ryansobol) 124 | - add option to use secure url for image in auth hash (@mkdynamic) 125 | - add option to specify image size (@mkdynamic) 126 | 127 | Changes: 128 | 129 | - have `raw_info` return an empty hash if the Facebook response returns false (#44, @brianjlandau) 130 | - prevent oauth2 from interpreting Facebook's expires field as `expires_in`, when it's really `expires_at` (#39, @watsonbox) 131 | - remove deprecated `offline_access` permission (@mkdynamic) 132 | - tidy up the `callback_url` option (@mkdynamic) 133 | 134 | ## 1.2.0 (2012-01-06) 135 | 136 | Features: 137 | 138 | - add `state` to authorization params (#19, @GermanDZ) 139 | 140 | Changes: 141 | 142 | - lock to `rack ~> 1.3.6` (@mkdynamic) 143 | 144 | ## 1.1.0 (2011-12-10) 145 | 146 | Features: 147 | 148 | - add `callback_url` option (#13, @gumayunov) 149 | - support for parsing code from signed request cookie (client-side flow) (@mkdynamic) 150 | 151 | ## 1.0.0 (2011-11-19) 152 | 153 | Features: 154 | 155 | - allow passing of display via option (@mkdynamic) 156 | 157 | Bugfixes: 158 | 159 | - fix `ten_mins_from_now` calculation (#7, @olegkovalenko) 160 | 161 | ## 1.0.0.rc2 (2011-11-11) 162 | 163 | Features: 164 | 165 | - allow passing `display` parameter (@mkdynamic) 166 | - included default scope (@mkdynamic) 167 | 168 | ## 1.0.0.rc1 (2011-10-29) 169 | 170 | - first public gem release (@mkdynamic) 171 | -------------------------------------------------------------------------------- /lib/omniauth/strategies/facebook.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth/strategies/oauth2' 2 | require 'omniauth/facebook/signed_request' 3 | require 'openssl' 4 | require 'rack/utils' 5 | require 'uri' 6 | 7 | module OmniAuth 8 | module Strategies 9 | class Facebook < OmniAuth::Strategies::OAuth2 10 | class NoAuthorizationCodeError < StandardError; end 11 | 12 | DEFAULT_SCOPE = 'email' 13 | DEFAULT_FACEBOOK_API_VERSION = 'v19.0'.freeze 14 | 15 | option :client_options, { 16 | site: "https://graph.facebook.com/#{DEFAULT_FACEBOOK_API_VERSION}", 17 | authorize_url: "https://www.facebook.com/#{DEFAULT_FACEBOOK_API_VERSION}/dialog/oauth", 18 | token_url: 'oauth/access_token' 19 | } 20 | 21 | option :access_token_options, { 22 | header_format: 'OAuth %s', 23 | param_name: 'access_token' 24 | } 25 | 26 | option :authorization_code_from_signed_request_in_cookie, nil 27 | 28 | option :authorize_options, [:scope, :display, :auth_type, :config_id] 29 | 30 | option :secure_image_url, true 31 | 32 | uid { raw_info['id'] } 33 | 34 | info do 35 | prune!({ 36 | 'nickname' => raw_info['username'], 37 | 'email' => raw_info['email'], 38 | 'name' => raw_info['name'], 39 | 'first_name' => raw_info['first_name'], 40 | 'last_name' => raw_info['last_name'], 41 | 'image' => image_url(uid, options), 42 | 'description' => raw_info['bio'], 43 | 'urls' => { 44 | 'Facebook' => raw_info['link'], 45 | 'Website' => raw_info['website'] 46 | }, 47 | 'location' => (raw_info['location'] || {})['name'], 48 | 'verified' => raw_info['verified'] 49 | }) 50 | end 51 | 52 | extra do 53 | hash = {} 54 | hash['raw_info'] = raw_info unless skip_info? 55 | prune! hash 56 | end 57 | 58 | def raw_info 59 | @raw_info ||= access_token.get('me', info_options).parsed || {} 60 | end 61 | 62 | def info_options 63 | params = {appsecret_proof: appsecret_proof} 64 | params.merge!({fields: (options[:info_fields] || 'name,email')}) 65 | params.merge!({locale: options[:locale]}) if options[:locale] 66 | 67 | { params: params } 68 | end 69 | 70 | def callback_phase 71 | with_authorization_code! do 72 | super 73 | end 74 | rescue NoAuthorizationCodeError => e 75 | fail!(:no_authorization_code, e) 76 | rescue OmniAuth::Facebook::SignedRequest::UnknownSignatureAlgorithmError => e 77 | fail!(:unknown_signature_algorithm, e) 78 | end 79 | 80 | # NOTE If we're using code from the signed request then FB sets the redirect_uri to '' during the authorize 81 | # phase and it must match during the access_token phase: 82 | # https://github.com/facebook/facebook-php-sdk/blob/master/src/base_facebook.php#L477 83 | def callback_url 84 | if options.authorization_code_from_signed_request_in_cookie 85 | '' 86 | else 87 | # Fixes regression in omniauth-oauth2 v1.4.0 by https://github.com/intridea/omniauth-oauth2/commit/85fdbe117c2a4400d001a6368cc359d88f40abc7 88 | options[:callback_url] || (full_host + callback_path) 89 | end 90 | end 91 | 92 | def access_token_options 93 | options.access_token_options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h } 94 | end 95 | 96 | # You can pass +display+, +scope+, +auth_type+ or +config_id+ params to the auth request, if you need to set them dynamically. 97 | # You can also set these options in the OmniAuth config :authorize_params option. 98 | # 99 | # For example: /auth/facebook?display=popup 100 | def authorize_params 101 | super.tap do |params| 102 | %w[display scope auth_type config_id].each do |v| 103 | if request.params[v] 104 | params[v.to_sym] = request.params[v] 105 | end 106 | end 107 | 108 | params[:scope] ||= DEFAULT_SCOPE 109 | end 110 | end 111 | 112 | protected 113 | 114 | def build_access_token 115 | super.tap do |token| 116 | token.options.merge!(access_token_options) 117 | end 118 | end 119 | 120 | private 121 | 122 | def signed_request_from_cookie 123 | @signed_request_from_cookie ||= raw_signed_request_from_cookie && OmniAuth::Facebook::SignedRequest.parse(raw_signed_request_from_cookie, client.secret) 124 | end 125 | 126 | def raw_signed_request_from_cookie 127 | request.cookies["fbsr_#{client.id}"] 128 | end 129 | 130 | # Picks the authorization code in order, from: 131 | # 132 | # 1. The request 'code' param (manual callback from standard server-side flow) 133 | # 2. A signed request from cookie (passed from the client during the client-side flow) 134 | def with_authorization_code! 135 | if request.params.key?('code') 136 | yield 137 | elsif code_from_signed_request = signed_request_from_cookie && signed_request_from_cookie['code'] 138 | request.params['code'] = code_from_signed_request 139 | options.authorization_code_from_signed_request_in_cookie = true 140 | # NOTE The code from the signed fbsr_XXX cookie is set by the FB JS SDK will confirm that the identity of the 141 | # user contained in the signed request matches the user loading the app. 142 | original_provider_ignores_state = options.provider_ignores_state 143 | options.provider_ignores_state = true 144 | begin 145 | yield 146 | ensure 147 | request.params.delete('code') 148 | options.authorization_code_from_signed_request_in_cookie = false 149 | options.provider_ignores_state = original_provider_ignores_state 150 | end 151 | else 152 | raise NoAuthorizationCodeError, 'must pass either a `code` (via URL or by an `fbsr_XXX` signed request cookie)' 153 | end 154 | end 155 | 156 | def prune!(hash) 157 | hash.delete_if do |_, value| 158 | prune!(value) if value.is_a?(Hash) 159 | value.nil? || (value.respond_to?(:empty?) && value.empty?) 160 | end 161 | end 162 | 163 | def image_url(uid, options) 164 | uri_class = options[:secure_image_url] ? URI::HTTPS : URI::HTTP 165 | site_uri = URI.parse(client.site) 166 | url = uri_class.build({host: site_uri.host, path: "#{site_uri.path}/#{uid}/picture"}) 167 | 168 | query = if options[:image_size].is_a?(String) || options[:image_size].is_a?(Symbol) 169 | { type: options[:image_size] } 170 | elsif options[:image_size].is_a?(Hash) 171 | options[:image_size] 172 | end 173 | url.query = Rack::Utils.build_query(query) if query 174 | 175 | url.to_s 176 | end 177 | 178 | def appsecret_proof 179 | @appsecret_proof ||= OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, client.secret, access_token.token) 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OmniAuth Facebook  [![Build Status](https://secure.travis-ci.org/simi/omniauth-facebook.svg?branch=master)](https://travis-ci.org/simi/omniauth-facebook) [![Gem Version](https://img.shields.io/gem/v/omniauth-facebook.svg)](https://rubygems.org/gems/omniauth-facebook) 2 | 3 | 📣 **NOTICE** We’re looking for maintainers to help keep this project up-to-date. If you are interested in helping please open an Issue expressing your interest. Thanks! 📣 4 | 5 | **These notes are based on master, please see tags for README pertaining to specific releases.** 6 | 7 | Facebook OAuth2 Strategy for OmniAuth. 8 | 9 | Supports OAuth 2.0 server-side and client-side flows. Read the Facebook docs for more details: http://developers.facebook.com/docs/authentication 10 | 11 | ## Installing 12 | 13 | Add to your `Gemfile`: 14 | 15 | ```ruby 16 | gem 'omniauth-facebook' 17 | ``` 18 | 19 | Then `bundle install`. 20 | 21 | ## Usage 22 | 23 | `OmniAuth::Strategies::Facebook` is simply a Rack middleware. Read the OmniAuth docs for detailed instructions: https://github.com/intridea/omniauth. 24 | 25 | Here's a quick example, adding the middleware to a Rails app in `config/initializers/omniauth.rb`: 26 | 27 | ```ruby 28 | Rails.application.config.middleware.use OmniAuth::Builder do 29 | provider :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'] 30 | end 31 | ``` 32 | 33 | [See the example Sinatra app for full examples](https://github.com/simi/omniauth-facebook/blob/master/example/config.ru) of both the server and client-side flows (including using the Facebook Javascript SDK). 34 | 35 | ## Configuring 36 | 37 | You can configure several options, which you pass in to the `provider` method via a `Hash`: 38 | 39 | Option name | Default | Explanation 40 | --- | --- | --- 41 | `scope` | `email` | A comma-separated list of permissions you want to request from the user. See the Facebook docs for a full list of available permissions: https://developers.facebook.com/docs/reference/login/ 42 | `display` | `page` | The display context to show the authentication page. Options are: `page`, `popup` and 43 | `config_id` | | The configuration ID to use for a System User access token with Facebook Login for Business. Read the Facebook docs for more details: https://developers.facebook.com/docs/facebook-login/facebook-login-for-business#invoke-a--login-dialog 44 | `touch`. Read the Facebook docs for more details: https://developers.facebook.com/docs/reference/dialogs/oauth/ 45 | `image_size` | `square` | Set the size for the returned image url in the auth hash. Valid options include `square` (50x50), `small` (50 pixels wide, variable height), `normal` (100 pixels wide, variable height), or `large` (about 200 pixels wide, variable height). Additionally, you can request a picture of a specific size by setting this option to a hash with `:width` and `:height` as keys. This will return an available profile picture closest to the requested size and requested aspect ratio. If only `:width` or `:height` is specified, we will return a picture whose width or height is closest to the requested size, respectively. 46 | `info_fields` | `name,email` | Specify exactly which fields should be returned when getting the user's info. Value should be a comma-separated string as per https://developers.facebook.com/docs/graph-api/reference/user/ (only `/me` endpoint). 47 | `locale` | | Specify locale which should be used when getting the user's info. Value should be locale string as per https://developers.facebook.com/docs/reference/api/locale/. 48 | `auth_type` | | Optionally specifies the requested authentication features as a comma-separated list, as per https://developers.facebook.com/docs/facebook-login/reauthentication/. Valid values are `https` (checks for the presence of the secure cookie and asks for re-authentication if it is not present), and `reauthenticate` (asks the user to re-authenticate unconditionally). Use 'rerequest' when you want to request premissions. Default is `nil`. 49 | `secure_image_url` | `true` | Set to `true` to use https for the avatar image url returned in the auth hash. SSL is mandatory as per https://developers.facebook.com/docs/facebook-login/security#surfacearea. 50 | `callback_url` / `callback_path` | | Specify a custom callback URL used during the server-side flow. Note this must be allowed by your app configuration on Facebook (see 'Valid OAuth redirect URIs' under the 'Advanced' settings section in the configuration for your Facebook app for more details). 51 | 52 | For example, to request `email`, `user_birthday` and `read_stream` permissions and display the authentication page in a popup window: 53 | 54 | ```ruby 55 | Rails.application.config.middleware.use OmniAuth::Builder do 56 | provider :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'], 57 | scope: 'email,user_birthday,read_stream', display: 'popup' 58 | end 59 | ``` 60 | 61 | ### API Version 62 | 63 | OmniAuth Facebook uses versioned API endpoints by default (current v19.0). You can configure a different version via `client_options` hash passed to `provider`, specifically you should change the version in the `site` and `authorize_url` parameters. For example, to change to v20.0 (assuming that exists): 64 | 65 | ```ruby 66 | use OmniAuth::Builder do 67 | provider :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'], 68 | client_options: { 69 | site: 'https://graph.facebook.com/v20.0', 70 | authorize_url: "https://www.facebook.com/v20.0/dialog/oauth" 71 | } 72 | end 73 | ``` 74 | 75 | ### Per-Request Options 76 | 77 | If you want to set the `display` format, `auth_type`, `scope` or `config_id` on a per-request basis, you can just pass it to the OmniAuth request phase URL, for example: `/auth/facebook?display=popup`, `/auth/facebook?scope=email` or `/auth/facebook?config_id=001`. 78 | 79 | ## Auth Hash 80 | 81 | Here's an example *Auth Hash* available in `request.env['omniauth.auth']`: 82 | 83 | ```ruby 84 | { 85 | provider: 'facebook', 86 | uid: '1234567', 87 | info: { 88 | email: 'joe@bloggs.com', 89 | name: 'Joe Bloggs', 90 | first_name: 'Joe', 91 | last_name: 'Bloggs', 92 | image: 'http://graph.facebook.com/1234567/picture?type=square', 93 | verified: true 94 | }, 95 | credentials: { 96 | token: 'ABCDEF...', # OAuth 2.0 access_token, which you may wish to store 97 | expires_at: 1321747205, # when the access token expires (it always will) 98 | expires: true # this will always be true 99 | }, 100 | extra: { 101 | raw_info: { 102 | id: '1234567', 103 | name: 'Joe Bloggs', 104 | first_name: 'Joe', 105 | last_name: 'Bloggs', 106 | link: 'http://www.facebook.com/jbloggs', 107 | username: 'jbloggs', 108 | location: { id: '123456789', name: 'Palo Alto, California' }, 109 | gender: 'male', 110 | email: 'joe@bloggs.com', 111 | timezone: -8, 112 | locale: 'en_US', 113 | verified: true, 114 | updated_time: '2011-11-11T06:21:03+0000', 115 | # ... 116 | } 117 | } 118 | } 119 | ``` 120 | 121 | The precise information available may depend on the permissions which you request. 122 | 123 | ## Client-side Flow with Facebook Javascript SDK 124 | 125 | You can use the Facebook Javascript SDK with `FB.login`, and just hit the callback endpoint (`/auth/facebook/callback` by default) once the user has authenticated in the success callback. 126 | 127 | **Note that you must enable cookies in the `FB.init` config for this process to work.** 128 | 129 | See the example Sinatra app under `example/` and read the [Facebook docs on Login for JavaScript](https://developers.facebook.com/docs/facebook-login/login-flow-for-web/) for more details. 130 | 131 | ### How it Works 132 | 133 | The client-side flow is supported by parsing the authorization code from the signed request which Facebook places in a cookie. 134 | 135 | When you call `/auth/facebook/callback` in the success callback of `FB.login` that will pass the cookie back to the server. omniauth-facebook will see this cookie and: 136 | 137 | 1. parse it, 138 | 2. extract the authorization code contained in it 139 | 3. and hit Facebook and obtain an access token which will get placed in the `request.env['omniauth.auth']['credentials']` hash. 140 | 141 | ## Token Expiry 142 | 143 | The expiration time of the access token you obtain will depend on which flow you are using. 144 | 145 | ### Client-Side Flow 146 | 147 | If you use the client-side flow, Facebook will give you back a short lived access token (~ 2 hours). 148 | 149 | You can exchange this short lived access token for a longer lived version. Read the [Facebook docs](https://developers.facebook.com/docs/facebook-login/access-tokens/) for more information on exchanging a short lived token for a long lived token. 150 | 151 | ### Server-Side Flow 152 | 153 | If you use the server-side flow, Facebook will give you back a longer lived access token (~ 60 days). 154 | 155 | ## Supported Rubies 156 | 157 | - Ruby MRI (3.0, 3.1, 3.2 and 3.3) 158 | 159 | ## License 160 | 161 | Copyright (c) 2012 by Mark Dodwell 162 | 163 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 164 | 165 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 166 | 167 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 168 | -------------------------------------------------------------------------------- /test/strategy_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require 'omniauth-facebook' 3 | require 'openssl' 4 | require 'base64' 5 | 6 | class StrategyTest < StrategyTestCase 7 | include OAuth2StrategyTests 8 | end 9 | 10 | class ClientTest < StrategyTestCase 11 | test 'has correct Facebook site' do 12 | assert_equal "https://graph.facebook.com/#{@facebook_api_version}", strategy.client.site 13 | end 14 | 15 | test 'has correct authorize url' do 16 | assert_equal "https://www.facebook.com/#{@facebook_api_version}/dialog/oauth", strategy.client.options[:authorize_url] 17 | end 18 | 19 | test 'has correct token url with versioning' do 20 | @options = {client_options: {site: 'https://graph.facebook.net/v2.2'}} 21 | assert_equal 'oauth/access_token', strategy.client.options[:token_url] 22 | assert_equal 'https://graph.facebook.net/v2.2/oauth/access_token', strategy.client.token_url 23 | end 24 | end 25 | 26 | class CallbackUrlTest < StrategyTestCase 27 | test "returns the default callback url (omitting querystring)" do 28 | url_base = 'http://auth.request.com' 29 | script_name = '/script_name' 30 | @request.stubs(:url).returns("#{url_base}/some/page") 31 | strategy.stubs(:script_name).returns(script_name) # as not to depend on Rack env 32 | strategy.stubs(:query_string).returns('?foo=bar') 33 | assert_equal "#{url_base}#{script_name}/auth/facebook/callback", strategy.callback_url 34 | end 35 | 36 | test "returns path from callback_path option (omitting querystring)" do 37 | @options = { callback_path: "/auth/FB/done"} 38 | url_base = 'http://auth.request.com' 39 | @request.stubs(:url).returns("#{url_base}/page/path") 40 | strategy.stubs(:script_name).returns('') # as not to depend on Rack env 41 | strategy.stubs(:query_string).returns('?foo=bar') 42 | assert_equal "#{url_base}/auth/FB/done", strategy.callback_url 43 | end 44 | 45 | test "returns url from callback_url option" do 46 | url = 'https://auth.myapp.com/auth/fb/callback' 47 | @options = { callback_url: url } 48 | assert_equal url, strategy.callback_url 49 | end 50 | end 51 | 52 | class AuthorizeParamsTest < StrategyTestCase 53 | test 'includes default scope for email' do 54 | assert strategy.authorize_params.is_a?(Hash) 55 | assert_equal 'email', strategy.authorize_params[:scope] 56 | end 57 | 58 | test 'includes display parameter from request when present' do 59 | @request.stubs(:params).returns({ 'display' => 'touch' }) 60 | assert strategy.authorize_params.is_a?(Hash) 61 | assert_equal 'touch', strategy.authorize_params[:display] 62 | end 63 | 64 | test 'includes config_id parameter from request when present' do 65 | @request.stubs(:params).returns({ 'config_id' => '000111222' }) 66 | assert strategy.authorize_params.is_a?(Hash) 67 | assert_equal '000111222', strategy.authorize_params[:config_id] 68 | end 69 | 70 | test 'includes auth_type parameter from request when present' do 71 | @request.stubs(:params).returns({ 'auth_type' => 'reauthenticate' }) 72 | assert strategy.authorize_params.is_a?(Hash) 73 | assert_equal 'reauthenticate', strategy.authorize_params[:auth_type] 74 | end 75 | 76 | test 'overrides default scope with parameter passed from request' do 77 | @request.stubs(:params).returns({ 'scope' => 'email' }) 78 | assert strategy.authorize_params.is_a?(Hash) 79 | assert_equal 'email', strategy.authorize_params[:scope] 80 | end 81 | end 82 | 83 | class AccessTokenOptionsTest < StrategyTestCase 84 | test 'has correct param name by default' do 85 | assert_equal 'access_token', strategy.access_token_options[:param_name] 86 | end 87 | 88 | test 'has correct header format by default' do 89 | assert_equal 'OAuth %s', strategy.access_token_options[:header_format] 90 | end 91 | end 92 | 93 | class UidTest < StrategyTestCase 94 | def setup 95 | super 96 | strategy.stubs(:raw_info).returns({ 'id' => '123' }) 97 | end 98 | 99 | test 'returns the id from raw_info' do 100 | assert_equal '123', strategy.uid 101 | end 102 | end 103 | 104 | class InfoTest < StrategyTestCase 105 | test 'returns the secure facebook avatar url when `secure_image_url` option is set to true' do 106 | @options = { secure_image_url: true } 107 | raw_info = { 'name' => 'Fred Smith', 'id' => '321' } 108 | strategy.stubs(:raw_info).returns(raw_info) 109 | assert_equal "https://graph.facebook.com/#{@facebook_api_version}/321/picture", strategy.info['image'] 110 | end 111 | 112 | test 'returns the non-ssl facebook avatar url when `secure_image_url` option is set to false' do 113 | @options = { secure_image_url: false } 114 | raw_info = { 'name' => 'Fred Smith', 'id' => '321' } 115 | strategy.stubs(:raw_info).returns(raw_info) 116 | assert_equal "http://graph.facebook.com/#{@facebook_api_version}/321/picture", strategy.info['image'] 117 | end 118 | 119 | test 'returns the secure facebook avatar url when `secure_image_url` option is omitted' do 120 | raw_info = { 'name' => 'Fred Smith', 'id' => '321' } 121 | strategy.stubs(:raw_info).returns(raw_info) 122 | assert_equal "https://graph.facebook.com/#{@facebook_api_version}/321/picture", strategy.info['image'] 123 | end 124 | 125 | test 'returns the image_url based of the client site' do 126 | @options = { secure_image_url: true, client_options: {site: "https://blah.facebook.com/v2.2"}} 127 | raw_info = { 'name' => 'Fred Smith', 'id' => '321' } 128 | strategy.stubs(:raw_info).returns(raw_info) 129 | assert_equal "https://blah.facebook.com/v2.2/321/picture", strategy.info['image'] 130 | end 131 | 132 | test 'returns the image with size specified in the `image_size` option' do 133 | @options = { image_size: 'normal' } 134 | raw_info = { 'name' => 'Fred Smith', 'id' => '321' } 135 | strategy.stubs(:raw_info).returns(raw_info) 136 | assert_equal "https://graph.facebook.com/#{@facebook_api_version}/321/picture?type=normal", strategy.info['image'] 137 | end 138 | 139 | test 'returns the image with size specified as a symbol in the `image_size` option' do 140 | @options = { image_size: :normal } 141 | raw_info = { 'name' => 'Fred Smith', 'id' => '321' } 142 | strategy.stubs(:raw_info).returns(raw_info) 143 | assert_equal "https://graph.facebook.com/#{@facebook_api_version}/321/picture?type=normal", strategy.info['image'] 144 | end 145 | 146 | test 'returns the image with width and height specified in the `image_size` option' do 147 | @options = { image_size: { width: 123, height: 987 } } 148 | raw_info = { 'name' => 'Fred Smith', 'id' => '321' } 149 | strategy.stubs(:raw_info).returns(raw_info) 150 | assert_match 'width=123', strategy.info['image'] 151 | assert_match 'height=987', strategy.info['image'] 152 | assert_match "https://graph.facebook.com/#{@facebook_api_version}/321/picture", strategy.info['image'] 153 | end 154 | end 155 | 156 | class InfoTestOptionalDataPresent < StrategyTestCase 157 | def setup 158 | super 159 | @raw_info ||= { 'name' => 'Fred Smith' } 160 | strategy.stubs(:raw_info).returns(@raw_info) 161 | end 162 | 163 | test 'returns the name' do 164 | assert_equal 'Fred Smith', strategy.info['name'] 165 | end 166 | 167 | test 'returns the email' do 168 | @raw_info['email'] = 'fred@smith.com' 169 | assert_equal 'fred@smith.com', strategy.info['email'] 170 | end 171 | 172 | test 'returns the username as nickname' do 173 | @raw_info['username'] = 'fredsmith' 174 | assert_equal 'fredsmith', strategy.info['nickname'] 175 | end 176 | 177 | test 'returns the first name' do 178 | @raw_info['first_name'] = 'Fred' 179 | assert_equal 'Fred', strategy.info['first_name'] 180 | end 181 | 182 | test 'returns the last name' do 183 | @raw_info['last_name'] = 'Smith' 184 | assert_equal 'Smith', strategy.info['last_name'] 185 | end 186 | 187 | test 'returns the location name as location' do 188 | @raw_info['location'] = { 'id' => '104022926303756', 'name' => 'Palo Alto, California' } 189 | assert_equal 'Palo Alto, California', strategy.info['location'] 190 | end 191 | 192 | test 'returns bio as description' do 193 | @raw_info['bio'] = 'I am great' 194 | assert_equal 'I am great', strategy.info['description'] 195 | end 196 | 197 | test 'returns the facebook avatar url' do 198 | @raw_info['id'] = '321' 199 | assert_equal "https://graph.facebook.com/#{@facebook_api_version}/321/picture", strategy.info['image'] 200 | end 201 | 202 | test 'returns the Facebook link as the Facebook url' do 203 | @raw_info['link'] = 'http://www.facebook.com/fredsmith' 204 | assert_kind_of Hash, strategy.info['urls'] 205 | assert_equal 'http://www.facebook.com/fredsmith', strategy.info['urls']['Facebook'] 206 | end 207 | 208 | test 'returns website url' do 209 | @raw_info['website'] = 'https://my-wonderful-site.com' 210 | assert_kind_of Hash, strategy.info['urls'] 211 | assert_equal 'https://my-wonderful-site.com', strategy.info['urls']['Website'] 212 | end 213 | 214 | test 'return both Facebook link and website urls' do 215 | @raw_info['link'] = 'http://www.facebook.com/fredsmith' 216 | @raw_info['website'] = 'https://my-wonderful-site.com' 217 | assert_kind_of Hash, strategy.info['urls'] 218 | assert_equal 'http://www.facebook.com/fredsmith', strategy.info['urls']['Facebook'] 219 | assert_equal 'https://my-wonderful-site.com', strategy.info['urls']['Website'] 220 | end 221 | 222 | test 'returns the positive verified status' do 223 | @raw_info['verified'] = true 224 | assert strategy.info['verified'] 225 | end 226 | 227 | test 'returns the negative verified status' do 228 | @raw_info['verified'] = false 229 | refute strategy.info['verified'] 230 | end 231 | end 232 | 233 | class InfoTestOptionalDataNotPresent < StrategyTestCase 234 | def setup 235 | super 236 | @raw_info ||= { 'name' => 'Fred Smith' } 237 | strategy.stubs(:raw_info).returns(@raw_info) 238 | end 239 | 240 | test 'has no email key' do 241 | refute_has_key 'email', strategy.info 242 | end 243 | 244 | test 'has no nickname key' do 245 | refute_has_key 'nickname', strategy.info 246 | end 247 | 248 | test 'has no first name key' do 249 | refute_has_key 'first_name', strategy.info 250 | end 251 | 252 | test 'has no last name key' do 253 | refute_has_key 'last_name', strategy.info 254 | end 255 | 256 | test 'has no location key' do 257 | refute_has_key 'location', strategy.info 258 | end 259 | 260 | test 'has no description key' do 261 | refute_has_key 'description', strategy.info 262 | end 263 | 264 | test 'has no urls' do 265 | refute_has_key 'urls', strategy.info 266 | end 267 | 268 | test 'has no verified key' do 269 | refute_has_key 'verified', strategy.info 270 | end 271 | end 272 | 273 | class RawInfoTest < StrategyTestCase 274 | def setup 275 | super 276 | @access_token = stub('OAuth2::AccessToken') 277 | @appsecret_proof = 'appsecret_proof' 278 | @options = {appsecret_proof: @appsecret_proof, fields: 'name,email'} 279 | end 280 | 281 | test "performs a GET to https://graph.facebook.com/#{@facebook_api_version}/me" do 282 | strategy.stubs(:appsecret_proof).returns(@appsecret_proof) 283 | strategy.stubs(:access_token).returns(@access_token) 284 | params = {params: @options} 285 | @access_token.expects(:get).with('me', params).returns(stub_everything('OAuth2::Response')) 286 | strategy.raw_info 287 | end 288 | 289 | test "performs a GET to https://graph.facebook.com/#{@facebook_api_version}/me with locale" do 290 | @options.merge!({ locale: 'cs_CZ' }) 291 | strategy.stubs(:access_token).returns(@access_token) 292 | strategy.stubs(:appsecret_proof).returns(@appsecret_proof) 293 | params = {params: @options} 294 | @access_token.expects(:get).with('me', params).returns(stub_everything('OAuth2::Response')) 295 | strategy.raw_info 296 | end 297 | 298 | test "performs a GET to https://graph.facebook.com/#{@facebook_api_version}/me with info_fields" do 299 | @options.merge!({info_fields: 'about'}) 300 | strategy.stubs(:access_token).returns(@access_token) 301 | strategy.stubs(:appsecret_proof).returns(@appsecret_proof) 302 | params = {params: {appsecret_proof: @appsecret_proof, fields: 'about'}} 303 | @access_token.expects(:get).with('me', params).returns(stub_everything('OAuth2::Response')) 304 | strategy.raw_info 305 | end 306 | 307 | test "performs a GET to https://graph.facebook.com/#{@facebook_api_version}/me with default info_fields" do 308 | strategy.stubs(:access_token).returns(@access_token) 309 | strategy.stubs(:appsecret_proof).returns(@appsecret_proof) 310 | params = {params: {appsecret_proof: @appsecret_proof, fields: 'name,email'}} 311 | @access_token.expects(:get).with('me', params).returns(stub_everything('OAuth2::Response')) 312 | strategy.raw_info 313 | end 314 | 315 | test 'returns a Hash' do 316 | strategy.stubs(:access_token).returns(@access_token) 317 | strategy.stubs(:appsecret_proof).returns(@appsecret_proof) 318 | raw_response = stub('Faraday::Response') 319 | raw_response.stubs(:body).returns('{ "ohai": "thar" }') 320 | raw_response.stubs(:status).returns(200) 321 | raw_response.stubs(:headers).returns({'Content-Type' => 'application/json' }) 322 | oauth2_response = OAuth2::Response.new(raw_response) 323 | params = {params: @options} 324 | @access_token.stubs(:get).with('me', params).returns(oauth2_response) 325 | assert_kind_of Hash, strategy.raw_info 326 | assert_equal 'thar', strategy.raw_info['ohai'] 327 | end 328 | 329 | test 'returns an empty hash when the response is false' do 330 | strategy.stubs(:access_token).returns(@access_token) 331 | strategy.stubs(:appsecret_proof).returns(@appsecret_proof) 332 | oauth2_response = stub('OAuth2::Response', parsed: false) 333 | params = {params: @options} 334 | @access_token.stubs(:get).with('me', params).returns(oauth2_response) 335 | assert_kind_of Hash, strategy.raw_info 336 | assert_equal({}, strategy.raw_info) 337 | end 338 | 339 | test 'should not include raw_info in extras hash when skip_info is specified' do 340 | @options = { skip_info: true } 341 | strategy.stubs(:raw_info).returns({foo: 'bar' }) 342 | refute_has_key 'raw_info', strategy.extra 343 | end 344 | end 345 | 346 | class CredentialsTest < StrategyTestCase 347 | def setup 348 | super 349 | @access_token = stub('OAuth2::AccessToken') 350 | @access_token.stubs(:token) 351 | @access_token.stubs(:expires?) 352 | @access_token.stubs(:expires_at) 353 | @access_token.stubs(:refresh_token) 354 | strategy.stubs(:access_token).returns(@access_token) 355 | end 356 | 357 | test 'returns a Hash' do 358 | assert_kind_of Hash, strategy.credentials 359 | end 360 | 361 | test 'returns the token' do 362 | @access_token.stubs(:token).returns('123') 363 | assert_equal '123', strategy.credentials['token'] 364 | end 365 | 366 | test 'returns the expiry status' do 367 | @access_token.stubs(:expires?).returns(true) 368 | assert strategy.credentials['expires'] 369 | 370 | @access_token.stubs(:expires?).returns(false) 371 | refute strategy.credentials['expires'] 372 | end 373 | 374 | test 'returns the refresh token and expiry time when expiring' do 375 | ten_mins_from_now = (Time.now + 600).to_i 376 | @access_token.stubs(:expires?).returns(true) 377 | @access_token.stubs(:refresh_token).returns('321') 378 | @access_token.stubs(:expires_at).returns(ten_mins_from_now) 379 | assert_equal '321', strategy.credentials['refresh_token'] 380 | assert_equal ten_mins_from_now, strategy.credentials['expires_at'] 381 | end 382 | 383 | test 'does not return the refresh token when test is nil and expiring' do 384 | @access_token.stubs(:expires?).returns(true) 385 | @access_token.stubs(:refresh_token).returns(nil) 386 | assert_nil strategy.credentials['refresh_token'] 387 | refute_has_key 'refresh_token', strategy.credentials 388 | end 389 | 390 | test 'does not return the refresh token when not expiring' do 391 | @access_token.stubs(:expires?).returns(false) 392 | @access_token.stubs(:refresh_token).returns('XXX') 393 | assert_nil strategy.credentials['refresh_token'] 394 | refute_has_key 'refresh_token', strategy.credentials 395 | end 396 | end 397 | 398 | class ExtraTest < StrategyTestCase 399 | def setup 400 | super 401 | @raw_info = { 'name' => 'Fred Smith' } 402 | strategy.stubs(:raw_info).returns(@raw_info) 403 | end 404 | 405 | test 'returns a Hash' do 406 | assert_kind_of Hash, strategy.extra 407 | end 408 | 409 | test 'contains raw info' do 410 | assert_equal({ 'raw_info' => @raw_info }, strategy.extra) 411 | end 412 | end 413 | 414 | module SignedRequestHelpers 415 | def signed_request(payload, secret) 416 | encoded_payload = base64_encode_url(JSON.dump(payload)) 417 | encoded_signature = base64_encode_url(signature(encoded_payload, secret)) 418 | [encoded_signature, encoded_payload].join('.') 419 | end 420 | 421 | def base64_encode_url(value) 422 | Base64.encode64(value).tr('+/', '-_').gsub(/\n/, '') 423 | end 424 | 425 | def signature(payload, secret, algorithm = OpenSSL::Digest::SHA256.new) 426 | OpenSSL::HMAC.digest(algorithm, secret, payload) 427 | end 428 | end 429 | 430 | module SignedRequestTests 431 | class TestCase < StrategyTestCase 432 | include SignedRequestHelpers 433 | end 434 | 435 | class CookieAndParamNotPresentTest < TestCase 436 | test 'is nil' do 437 | assert_nil strategy.send(:signed_request_from_cookie) 438 | end 439 | 440 | test 'throws an error on calling build_access_token' do 441 | assert_raises(OmniAuth::Strategies::Facebook::NoAuthorizationCodeError) { strategy.send(:with_authorization_code!) {} } 442 | end 443 | end 444 | 445 | class CookiePresentTest < TestCase 446 | def setup(algo = nil) 447 | super() 448 | @payload = { 449 | 'algorithm' => algo || 'HMAC-SHA256', 450 | 'code' => 'm4c0d3z', 451 | 'issued_at' => Time.now.to_i, 452 | 'user_id' => '123456' 453 | } 454 | 455 | @request.stubs(:cookies).returns({"fbsr_#{@client_id}" => signed_request(@payload, @client_secret)}) 456 | end 457 | 458 | test 'parses the access code out from the cookie' do 459 | assert_equal @payload, strategy.send(:signed_request_from_cookie) 460 | end 461 | 462 | test 'throws an error if the algorithm is unknown' do 463 | setup('UNKNOWN-ALGO') 464 | assert_equal "unknown algorithm: UNKNOWN-ALGO", assert_raises(OmniAuth::Facebook::SignedRequest::UnknownSignatureAlgorithmError) { strategy.send(:signed_request_from_cookie) }.message 465 | end 466 | end 467 | 468 | class EmptySignedRequestTest < TestCase 469 | def setup 470 | super 471 | @request.stubs(:params).returns({'signed_request' => ''}) 472 | end 473 | 474 | test 'empty param' do 475 | assert_nil strategy.send(:signed_request_from_cookie) 476 | end 477 | end 478 | 479 | class MissingCodeInParamsRequestTest < TestCase 480 | def setup 481 | super 482 | @request.stubs(:params).returns({}) 483 | end 484 | 485 | test 'calls fail! when a code is not included in the params' do 486 | strategy.expects(:fail!).times(1).with(:no_authorization_code, kind_of(OmniAuth::Strategies::Facebook::NoAuthorizationCodeError)) 487 | strategy.callback_phase 488 | end 489 | end 490 | 491 | class MissingCodeInCookieRequestTest < TestCase 492 | def setup(algo = nil) 493 | super() 494 | @payload = { 495 | 'algorithm' => algo || 'HMAC-SHA256', 496 | 'code' => nil, 497 | 'issued_at' => Time.now.to_i, 498 | 'user_id' => '123456' 499 | } 500 | 501 | @request.stubs(:cookies).returns({"fbsr_#{@client_id}" => signed_request(@payload, @client_secret)}) 502 | end 503 | 504 | test 'calls fail! when a code is not included in the cookie' do 505 | strategy.expects(:fail!).times(1).with(:no_authorization_code, kind_of(OmniAuth::Strategies::Facebook::NoAuthorizationCodeError)) 506 | strategy.callback_phase 507 | end 508 | end 509 | 510 | class UnknownAlgorithmInCookieRequestTest < TestCase 511 | def setup 512 | super() 513 | @payload = { 514 | 'algorithm' => 'UNKNOWN-ALGO', 515 | 'code' => nil, 516 | 'issued_at' => Time.now.to_i, 517 | 'user_id' => '123456' 518 | } 519 | 520 | @request.stubs(:cookies).returns({"fbsr_#{@client_id}" => signed_request(@payload, @client_secret)}) 521 | end 522 | 523 | test 'calls fail! when an algorithm is unknown' do 524 | strategy.expects(:fail!).times(1).with(:unknown_signature_algorithm, kind_of(OmniAuth::Facebook::SignedRequest::UnknownSignatureAlgorithmError)) 525 | strategy.callback_phase 526 | end 527 | end 528 | end 529 | --------------------------------------------------------------------------------