Codebase list ruby-omniauth-facebook / 260cb64
Imported Upstream version 1.4.1 Nitesh A Jain 11 years ago
17 changed file(s) with 1392 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 *.gem
1 .bundle
2 .rspec
3 /Gemfile.lock
4 pkg/*
5 .powenv
6 tmp
7 bin
0 rvm:
1 - 1.8.7
2 - 1.9.2
3 - 1.9.3
4 - jruby
5 branches:
6 only:
7 - master
0 source :rubygems
1
2 gemspec
3
4 gem 'jruby-openssl', :platform => :jruby
0 # OmniAuth Facebook  [![Build Status](http://travis-ci.org/mkdynamic/omniauth-facebook.png?branch=master)](http://travis-ci.org/mkdynamic/omniauth-facebook)
1
2 Facebook OAuth2 Strategy for OmniAuth 1.0.
3
4 Supports the OAuth 2.0 server-side and client-side flows. Read the Facebook docs for more details: http://developers.facebook.com/docs/authentication
5
6 ## Installing
7
8 Add to your `Gemfile`:
9
10 ```ruby
11 gem 'omniauth-facebook'
12 ```
13
14 Then `bundle install`.
15
16 ## Usage
17
18 `OmniAuth::Strategies::Facebook` is simply a Rack middleware. Read the OmniAuth 1.0 docs for detailed instructions: https://github.com/intridea/omniauth.
19
20 Here's a quick example, adding the middleware to a Rails app in `config/initializers/omniauth.rb`:
21
22 ```ruby
23 Rails.application.config.middleware.use OmniAuth::Builder do
24 provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
25 end
26 ```
27
28 [See the example Sinatra app for full examples](https://github.com/mkdynamic/omniauth-facebook/blob/master/example/config.ru) of both the server and client-side flows (including using the Facebook Javascript SDK).
29
30 ## Configuring
31
32 You can configure several options, which you pass in to the `provider` method via a `Hash`:
33
34 * `scope`: A comma-separated list of permissions you want to request from the user. See the Facebook docs for a full list of available permissions: http://developers.facebook.com/docs/reference/api/permissions. Default: `email`
35 * `display`: The display context to show the authentication page. Options are: `page`, `popup` and `touch`. Read the Facebook docs for more details: https://developers.facebook.com/docs/reference/dialogs/oauth/. Default: `page`
36 * `auth_type`: Optionally specifies the requested authentication features as a comma-separated list, as per https://developers.facebook.com/docs/authentication/reauthentication/.
37 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). Default is `nil`.
38 * `secure_image_url`: Set to `true` to use https for the avatar image url returned in the auth hash. Default is `false`.
39 * `image_size`: Set the size for the returned image url in the auth hash. Valid options are `square` (50x50), `small` (50 pixels wide, variable height), `normal` (100 pixels wide, variable height), or `large` (about 200 pixels wide, variable height). Default is `square` (50x50).
40
41 For example, to request `email`, `user_birthday` and `read_stream` permissions and display the authentication page in a popup window:
42
43 ```ruby
44 Rails.application.config.middleware.use OmniAuth::Builder do
45 provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'],
46 :scope => 'email,user_birthday,read_stream', :display => 'popup'
47 end
48 ```
49
50 ### Per-Request Options
51
52 If you want to set the `display` format or `scope` on a per-request basis, you can just pass it to the OmniAuth request phase URL, for example: `/auth/facebook?display=popup` or `/auth/facebook?scope=email`.
53
54 You can also pass through a `state` param which will be passed along to the callback url.
55
56 ### Custom Callback URL/Path
57
58 You can set a custom `callback_url` or `callback_path` option to override the default value. See [OmniAuth::Strategy#callback_url](https://github.com/intridea/omniauth/blob/master/lib/omniauth/strategy.rb#L411) for more details on the default.
59
60 ## Auth Hash
61
62 Here's an example *Auth Hash* available in `request.env['omniauth.auth']`:
63
64 ```ruby
65 {
66 :provider => 'facebook',
67 :uid => '1234567',
68 :info => {
69 :nickname => 'jbloggs',
70 :email => 'joe@bloggs.com',
71 :name => 'Joe Bloggs',
72 :first_name => 'Joe',
73 :last_name => 'Bloggs',
74 :image => 'http://graph.facebook.com/1234567/picture?type=square',
75 :urls => { :Facebook => 'http://www.facebook.com/jbloggs' },
76 :location => 'Palo Alto, California',
77 :verified => true
78 },
79 :credentials => {
80 :token => 'ABCDEF...', # OAuth 2.0 access_token, which you may wish to store
81 :expires_at => 1321747205, # when the access token expires (it always will)
82 :expires => true # this will always be true
83 },
84 :extra => {
85 :raw_info => {
86 :id => '1234567',
87 :name => 'Joe Bloggs',
88 :first_name => 'Joe',
89 :last_name => 'Bloggs',
90 :link => 'http://www.facebook.com/jbloggs',
91 :username => 'jbloggs',
92 :location => { :id => '123456789', :name => 'Palo Alto, California' },
93 :gender => 'male',
94 :email => 'joe@bloggs.com',
95 :timezone => -8,
96 :locale => 'en_US',
97 :verified => true,
98 :updated_time => '2011-11-11T06:21:03+0000'
99 }
100 }
101 }
102 ```
103
104 The precise information available may depend on the permissions which you request.
105
106 ## Client-side Flow
107
108 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.
109
110 Note that you must enable cookies in the `FB.init` config for this process to work.
111
112 See the example Sinatra app under `example/` and read the [Facebook docs on Client-Side Authentication](https://developers.facebook.com/docs/authentication/client-side/) for more details.
113
114 ### How it Works
115
116 The client-side flow is supported by parsing the authorization code from the signed request which Facebook places in a cookie.
117
118 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:
119
120 1. parse it,
121 2. extract the authorization code contained in it
122 3. and hit Facebook and obtain an access token which will get placed in the `request.env['omniauth.auth']['credentials']` hash.
123
124 Note that this access token will be the same token obtained and available in the client through the hash [as detailed in the Facebook docs](https://developers.facebook.com/docs/authentication/client-side/).
125
126 ## Canvas Apps
127
128 Canvas apps will send a signed request with the initial POST, therefore you *can* (if it makes sense for your app) pass this to the authorize endpoint (`/auth/facebook` by default) in the querystring.
129
130 There are then 2 scenarios for what happens next:
131
132 1. A user has already granted access to your app, this will contain an access token. In this case, omniauth-facebook will skip asking the user for authentication and immediately redirect to the callback endpoint (`/auth/facebook/callback` by default) with the access token present in the `request.env['omniauth.auth']['credentials']` hash.
133
134 2. A user has not granted access to your app, and the signed request *will not* contain an access token. In this case omniauth-facebook will simply follow the standard auth flow.
135
136 Take a look at [the example Sinatra app for one option of how you can integrate with a canvas page](https://github.com/mkdynamic/omniauth-facebook/blob/master/example/config.ru).
137
138 Bear in mind you have several options (including [authenticated referrals](https://developers.facebook.com/docs/opengraph/authentication/#referrals)). Read [the Facebook docs on canvas page authentication](https://developers.facebook.com/docs/authentication/canvas/) for more info.
139
140 ## Token Expiry
141
142 Since Facebook deprecated the `offline_access` permission, this has become more complex. The expiration time of the access token you obtain will depend on which flow you are using. See below for more details.
143
144 ### Client-Side Flow
145
146 If you use the client-side flow, Facebook will give you back a short lived access token (~ 2 hours).
147
148 You can exchange this short lived access token for a longer lived version. Read the [Facebook docs about the offline_access deprecation](https://developers.facebook.com/roadmap/offline-access-removal/) for more information.
149
150 ### Server-Side Flow
151
152 If you use the server-side flow, Facebook will give you back a longer loved access token (~ 60 days).
153
154 If you're having issue getting a long lived token with the server-side flow, make sure to enable the 'deprecate offline_access setting' in you Facebook app config. Read the [Facebook docs about the offline_access deprecation](https://developers.facebook.com/roadmap/offline-access-removal/) for more information.
155
156 ## Supported Rubies
157
158 Actively tested with the following Ruby versions:
159
160 - MRI 1.9.3
161 - MRI 1.9.2
162 - MRI 1.8.7
163 - JRuby 1.6.5
164
165 *NB.* For JRuby, you'll need to install the `jruby-openssl` gem. There's no way to automatically specify this in a Rubygem gemspec, so you need to manually add it your project's own Gemfile:
166
167 ```ruby
168 gem 'jruby-openssl', :platform => :jruby
169 ```
170
171 ## License
172
173 Copyright (c) 2012 by Mark Dodwell
174
175 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:
176
177 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
178
179 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.
0 require 'bundler/gem_tasks'
1 require 'rake/testtask'
2
3 Rake::TestTask.new do |task|
4 task.libs << 'test'
5 end
6
7 task :default => :test
0 source :rubygems
1
2 gem 'sinatra'
3 gem 'omniauth-facebook', :path => '../'
0 PATH
1 remote: ../
2 specs:
3 omniauth-facebook (1.4.0)
4 omniauth-oauth2 (~> 1.1.0)
5
6 GEM
7 remote: http://rubygems.org/
8 specs:
9 faraday (0.8.1)
10 multipart-post (~> 1.1)
11 hashie (1.2.0)
12 httpauth (0.1)
13 json (1.7.3)
14 jwt (0.1.4)
15 json (>= 1.2.4)
16 multi_json (1.3.6)
17 multipart-post (1.1.5)
18 oauth2 (0.8.0)
19 faraday (~> 0.8)
20 httpauth (~> 0.1)
21 jwt (~> 0.1.4)
22 multi_json (~> 1.0)
23 rack (~> 1.2)
24 omniauth (1.1.0)
25 hashie (~> 1.2)
26 rack
27 omniauth-oauth2 (1.1.0)
28 oauth2 (~> 0.8.0)
29 omniauth (~> 1.0)
30 rack (1.4.1)
31 rack-protection (1.2.0)
32 rack
33 sinatra (1.3.2)
34 rack (~> 1.3, >= 1.3.6)
35 rack-protection (~> 1.2)
36 tilt (~> 1.3, >= 1.3.3)
37 tilt (1.3.3)
38
39 PLATFORMS
40 ruby
41
42 DEPENDENCIES
43 omniauth-facebook!
44 sinatra
0 require 'bundler/setup'
1 require 'sinatra/base'
2 require 'omniauth-facebook'
3
4 SCOPE = 'email,read_stream'
5
6 class App < Sinatra::Base
7 # turn off sinatra default X-Frame-Options for FB canvas
8 set :protection, :except => :frame_options
9
10 # server-side flow
11 get '/' do
12 # NOTE: you would just hit this endpoint directly from the browser
13 # in a real app. the redirect is just here to setup the root
14 # path in this example sinatra app.
15 redirect '/auth/facebook'
16 end
17
18 # client-side flow
19 get '/client-side' do
20 content_type 'text/html'
21 # NOTE: when you enable cookie below in the FB.init call
22 # the GET request in the FB.login callback will send
23 # a signed request in a cookie back the OmniAuth callback
24 # which will parse out the authorization code and obtain
25 # the access_token. This will be the exact same access_token
26 # returned to the client in response.authResponse.accessToken.
27 <<-END
28 <html>
29 <head>
30 <title>Client-side Flow Example</title>
31 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js" type="text/javascript"></script>
32 </head>
33 <body>
34 <div id="fb-root"></div>
35
36 <script type="text/javascript">
37 window.fbAsyncInit = function() {
38 FB.init({
39 appId : '#{ENV['APP_ID']}',
40 status : true, // check login status
41 cookie : true, // enable cookies to allow the server to access the session
42 xfbml : true // parse XFBML
43 });
44 };
45
46 (function(d) {
47 var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {return;}
48 js = d.createElement('script'); js.id = id; js.async = true;
49 js.src = "//connect.facebook.net/en_US/all.js";
50 d.getElementsByTagName('head')[0].appendChild(js);
51 }(document));
52
53 $(function() {
54 $('a').click(function(e) {
55 e.preventDefault();
56
57 FB.login(function(response) {
58 if (response.authResponse) {
59 $('#connect').html('Connected! Hitting OmniAuth callback (GET /auth/facebook/callback)...');
60
61 // since we have cookies enabled, this request will allow omniauth to parse
62 // out the auth code from the signed request in the fbsr_XXX cookie
63 $.getJSON('/auth/facebook/callback', function(json) {
64 $('#connect').html('Connected! Callback complete.');
65 $('#results').html(JSON.stringify(json));
66 });
67 }
68 }, { scope: '#{SCOPE}' });
69 });
70 });
71 </script>
72
73 <p id="connect">
74 <a href="#">Connect to FB</a>
75 </p>
76
77 <p id="results" />
78 </body>
79 </html>
80 END
81 end
82
83 # auth via FB canvas and signed request param
84 post '/canvas/' do
85 # we just redirect to /auth/facebook here which will parse the
86 # signed_request FB sends us, asking for auth if the user has
87 # not already granted access, or simply moving straight to the
88 # callback where they have already granted access.
89 #
90 # we pass the state parameter which we can detect in our callback
91 # to do custom rendering/redirection for the canvas app page
92 redirect "/auth/facebook?signed_request=#{request.params['signed_request']}&state=canvas"
93 end
94
95 get '/auth/:provider/callback' do
96 # we can do something special here is +state+ param is canvas
97 # (see notes above in /canvas/ method for more details)
98 content_type 'application/json'
99 MultiJson.encode(request.env)
100 end
101
102 get '/auth/failure' do
103 content_type 'application/json'
104 MultiJson.encode(request.env)
105 end
106 end
107
108 use Rack::Session::Cookie
109
110 use OmniAuth::Builder do
111 provider :facebook, ENV['APP_ID'], ENV['APP_SECRET'], :scope => SCOPE
112 end
113
114 run App.new
0 module OmniAuth
1 module Facebook
2 VERSION = "1.4.1"
3 end
4 end
0 require 'omniauth/facebook/version'
1 require 'omniauth/strategies/facebook'
0 require 'omniauth/strategies/oauth2'
1 require 'base64'
2 require 'openssl'
3 require 'rack/utils'
4
5 module OmniAuth
6 module Strategies
7 class Facebook < OmniAuth::Strategies::OAuth2
8 class NoAuthorizationCodeError < StandardError; end
9
10 DEFAULT_SCOPE = 'email'
11
12 option :client_options, {
13 :site => 'https://graph.facebook.com',
14 :token_url => '/oauth/access_token'
15 }
16
17 option :token_params, {
18 :parse => :query
19 }
20
21 option :access_token_options, {
22 :header_format => 'OAuth %s',
23 :param_name => 'access_token'
24 }
25
26 option :authorize_options, [:scope, :display, :auth_type]
27
28 uid { raw_info['id'] }
29
30 info do
31 prune!({
32 'nickname' => raw_info['username'],
33 'email' => raw_info['email'],
34 'name' => raw_info['name'],
35 'first_name' => raw_info['first_name'],
36 'last_name' => raw_info['last_name'],
37 'image' => "#{options[:secure_image_url] ? 'https' : 'http'}://graph.facebook.com/#{uid}/picture?type=#{options[:image_size] || 'square'}",
38 'description' => raw_info['bio'],
39 'urls' => {
40 'Facebook' => raw_info['link'],
41 'Website' => raw_info['website']
42 },
43 'location' => (raw_info['location'] || {})['name'],
44 'verified' => raw_info['verified']
45 })
46 end
47
48 extra do
49 hash = {}
50 hash['raw_info'] = raw_info unless skip_info?
51 prune! hash
52 end
53
54 def raw_info
55 @raw_info ||= access_token.get('/me').parsed || {}
56 end
57
58 def build_access_token
59 if access_token = request.params["access_token"]
60 ::OAuth2::AccessToken.from_hash(
61 client,
62 {"access_token" => access_token}.update(access_token_options)
63 )
64 elsif signed_request_contains_access_token?
65 hash = signed_request.clone
66 ::OAuth2::AccessToken.new(
67 client,
68 hash.delete('oauth_token'),
69 hash.merge!(access_token_options.merge(:expires_at => hash.delete('expires')))
70 )
71 else
72 with_authorization_code! { super }.tap do |token|
73 token.options.merge!(access_token_options)
74 end
75 end
76 end
77
78 def request_phase
79 if signed_request_contains_access_token?
80 # if we already have an access token, we can just hit the
81 # callback URL directly and pass the signed request along
82 params = { :signed_request => raw_signed_request }
83 params[:state] = request.params['state'] if request.params['state']
84 query = Rack::Utils.build_query(params)
85
86 url = callback_url
87 url << "?" unless url.match(/\?/)
88 url << "&" unless url.match(/[\&\?]$/)
89 url << query
90
91 redirect url
92 else
93 super
94 end
95 end
96
97 # NOTE if we're using code from the signed request
98 # then FB sets the redirect_uri to '' during the authorize
99 # phase + it must match during the access_token phase:
100 # https://github.com/facebook/php-sdk/blob/master/src/base_facebook.php#L348
101 def callback_url
102 if @authorization_code_from_signed_request
103 ''
104 else
105 options[:callback_url] || super
106 end
107 end
108
109 def access_token_options
110 options.access_token_options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
111 end
112
113 ##
114 # You can pass +display+, +state+ or +scope+ params to the auth request, if
115 # you need to set them dynamically. You can also set these options
116 # in the OmniAuth config :authorize_params option.
117 #
118 # /auth/facebook?display=popup&state=ABC
119 #
120 def authorize_params
121 super.tap do |params|
122 %w[display state scope].each do |v|
123 if request.params[v]
124 params[v.to_sym] = request.params[v]
125
126 # to support omniauth-oauth2's auto csrf protection
127 session['omniauth.state'] = params[:state] if v == 'state'
128 end
129 end
130
131 params[:scope] ||= DEFAULT_SCOPE
132 end
133 end
134
135 ##
136 # Parse signed request in order, from:
137 #
138 # 1. the request 'signed_request' param (server-side flow from canvas pages) or
139 # 2. a cookie (client-side flow via JS SDK)
140 #
141 def signed_request
142 @signed_request ||= raw_signed_request &&
143 parse_signed_request(raw_signed_request)
144 end
145
146 private
147
148 def raw_signed_request
149 request.params['signed_request'] ||
150 request.cookies["fbsr_#{client.id}"]
151 end
152
153 ##
154 # If the signed_request comes from a FB canvas page and the user
155 # has already authorized your application, the JSON object will be
156 # contain the access token.
157 #
158 # https://developers.facebook.com/docs/authentication/canvas/
159 #
160 def signed_request_contains_access_token?
161 signed_request &&
162 signed_request['oauth_token']
163 end
164
165 ##
166 # Picks the authorization code in order, from:
167 #
168 # 1. the request 'code' param (manual callback from standard server-side flow)
169 # 2. a signed request (see #signed_request for more)
170 #
171 def with_authorization_code!
172 if request.params.key?('code')
173 yield
174 elsif code_from_signed_request = signed_request && signed_request['code']
175 request.params['code'] = code_from_signed_request
176 @authorization_code_from_signed_request = true
177 begin
178 yield
179 ensure
180 request.params.delete('code')
181 @authorization_code_from_signed_request = false
182 end
183 else
184 raise NoAuthorizationCodeError, 'must pass either a `code` parameter or a signed request (via `signed_request` parameter or a `fbsr_XXX` cookie)'
185 end
186 end
187
188 def prune!(hash)
189 hash.delete_if do |_, value|
190 prune!(value) if value.is_a?(Hash)
191 value.nil? || (value.respond_to?(:empty?) && value.empty?)
192 end
193 end
194
195 def parse_signed_request(value)
196 signature, encoded_payload = value.split('.')
197
198 decoded_hex_signature = base64_decode_url(signature)
199 decoded_payload = MultiJson.decode(base64_decode_url(encoded_payload))
200
201 unless decoded_payload['algorithm'] == 'HMAC-SHA256'
202 raise NotImplementedError, "unkown algorithm: #{decoded_payload['algorithm']}"
203 end
204
205 if valid_signature?(client.secret, decoded_hex_signature, encoded_payload)
206 decoded_payload
207 end
208 end
209
210 def valid_signature?(secret, signature, payload, algorithm = OpenSSL::Digest::SHA256.new)
211 OpenSSL::HMAC.digest(algorithm, secret, payload) == signature
212 end
213
214 def base64_decode_url(value)
215 value += '=' * (4 - value.size.modulo(4))
216 Base64.decode64(value.tr('-_', '+/'))
217 end
218 end
219 end
220 end
0 require 'omniauth/facebook'
0 --- !ruby/object:Gem::Specification
1 name: omniauth-facebook
2 version: !ruby/object:Gem::Version
3 version: 1.4.1
4 prerelease:
5 platform: ruby
6 authors:
7 - Mark Dodwell
8 autorequire:
9 bindir: bin
10 cert_chain: []
11 date: 2012-07-07 00:00:00.000000000 Z
12 dependencies:
13 - !ruby/object:Gem::Dependency
14 name: omniauth-oauth2
15 requirement: !ruby/object:Gem::Requirement
16 none: false
17 requirements:
18 - - ~>
19 - !ruby/object:Gem::Version
20 version: 1.1.0
21 type: :runtime
22 prerelease: false
23 version_requirements: !ruby/object:Gem::Requirement
24 none: false
25 requirements:
26 - - ~>
27 - !ruby/object:Gem::Version
28 version: 1.1.0
29 - !ruby/object:Gem::Dependency
30 name: minitest
31 requirement: !ruby/object:Gem::Requirement
32 none: false
33 requirements:
34 - - ! '>='
35 - !ruby/object:Gem::Version
36 version: '0'
37 type: :development
38 prerelease: false
39 version_requirements: !ruby/object:Gem::Requirement
40 none: false
41 requirements:
42 - - ! '>='
43 - !ruby/object:Gem::Version
44 version: '0'
45 - !ruby/object:Gem::Dependency
46 name: mocha
47 requirement: !ruby/object:Gem::Requirement
48 none: false
49 requirements:
50 - - ! '>='
51 - !ruby/object:Gem::Version
52 version: '0'
53 type: :development
54 prerelease: false
55 version_requirements: !ruby/object:Gem::Requirement
56 none: false
57 requirements:
58 - - ! '>='
59 - !ruby/object:Gem::Version
60 version: '0'
61 - !ruby/object:Gem::Dependency
62 name: rake
63 requirement: !ruby/object:Gem::Requirement
64 none: false
65 requirements:
66 - - ! '>='
67 - !ruby/object:Gem::Version
68 version: '0'
69 type: :development
70 prerelease: false
71 version_requirements: !ruby/object:Gem::Requirement
72 none: false
73 requirements:
74 - - ! '>='
75 - !ruby/object:Gem::Version
76 version: '0'
77 description:
78 email:
79 - mark@mkdynamic.co.uk
80 executables: []
81 extensions: []
82 extra_rdoc_files: []
83 files:
84 - .gitignore
85 - .travis.yml
86 - Gemfile
87 - README.md
88 - Rakefile
89 - example/Gemfile
90 - example/Gemfile.lock
91 - example/config.ru
92 - lib/omniauth-facebook.rb
93 - lib/omniauth/facebook.rb
94 - lib/omniauth/facebook/version.rb
95 - lib/omniauth/strategies/facebook.rb
96 - omniauth-facebook.gemspec
97 - test/helper.rb
98 - test/support/shared_examples.rb
99 - test/test.rb
100 homepage: https://github.com/mkdynamic/omniauth-facebook
101 licenses: []
102 post_install_message:
103 rdoc_options: []
104 require_paths:
105 - lib
106 required_ruby_version: !ruby/object:Gem::Requirement
107 none: false
108 requirements:
109 - - ! '>='
110 - !ruby/object:Gem::Version
111 version: '0'
112 segments:
113 - 0
114 hash: 1875274478054024285
115 required_rubygems_version: !ruby/object:Gem::Requirement
116 none: false
117 requirements:
118 - - ! '>='
119 - !ruby/object:Gem::Version
120 version: '0'
121 segments:
122 - 0
123 hash: 1875274478054024285
124 requirements: []
125 rubyforge_project:
126 rubygems_version: 1.8.24
127 signing_key:
128 specification_version: 3
129 summary: Facebook strategy for OmniAuth
130 test_files:
131 - test/helper.rb
132 - test/support/shared_examples.rb
133 - test/test.rb
0 # -*- encoding: utf-8 -*-
1 $:.push File.expand_path('../lib', __FILE__)
2 require 'omniauth/facebook/version'
3
4 Gem::Specification.new do |s|
5 s.name = 'omniauth-facebook'
6 s.version = OmniAuth::Facebook::VERSION
7 s.authors = ['Mark Dodwell']
8 s.email = ['mark@mkdynamic.co.uk']
9 s.summary = 'Facebook strategy for OmniAuth'
10 s.homepage = 'https://github.com/mkdynamic/omniauth-facebook'
11
12 s.files = `git ls-files`.split("\n")
13 s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14 s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
15 s.require_paths = ['lib']
16
17 s.add_runtime_dependency 'omniauth-oauth2', '~> 1.1.0'
18
19 s.add_development_dependency 'minitest'
20 s.add_development_dependency 'mocha'
21 s.add_development_dependency 'rake'
22 end
0 require 'bundler/setup'
1 require 'minitest/autorun'
2 require 'mocha'
3 require 'omniauth/strategies/facebook'
4
5 OmniAuth.config.test_mode = true
6
7 module BlockTestHelper
8 def test(name, &blk)
9 method_name = "test_#{name.gsub(/\s+/, '_')}"
10 raise "Method already defined: #{method_name}" if instance_methods.include?(method_name.to_sym)
11 define_method method_name, &blk
12 end
13 end
14
15 module CustomAssertions
16 def assert_has_key(key, hash, msg = nil)
17 msg = message(msg) { "Expected #{hash.inspect} to have key #{key.inspect}" }
18 assert hash.has_key?(key), msg
19 end
20
21 def refute_has_key(key, hash, msg = nil)
22 msg = message(msg) { "Expected #{hash.inspect} not to have key #{key.inspect}" }
23 refute hash.has_key?(key), msg
24 end
25 end
26
27 class TestCase < MiniTest::Unit::TestCase
28 extend BlockTestHelper
29 include CustomAssertions
30 end
31
32 class StrategyTestCase < TestCase
33 def setup
34 @request = stub('Request')
35 @request.stubs(:params).returns({})
36 @request.stubs(:cookies).returns({})
37 @request.stubs(:env).returns({})
38
39 @client_id = '123'
40 @client_secret = '53cr3tz'
41 end
42
43 def strategy
44 @strategy ||= begin
45 args = [@client_id, @client_secret, @options].compact
46 OmniAuth::Strategies::Facebook.new(nil, *args).tap do |strategy|
47 strategy.stubs(:request).returns(@request)
48 end
49 end
50 end
51 end
52
53 Dir[File.expand_path('../support/**/*', __FILE__)].each &method(:require)
0 # NOTE it would be useful if this lived in omniauth-oauth2 eventually
1 module OAuth2StrategyTests
2 def self.included(base)
3 base.class_eval do
4 include ClientTests
5 include AuthorizeParamsTests
6 include CSRFAuthorizeParamsTests
7 include TokenParamsTests
8 end
9 end
10
11 module ClientTests
12 extend BlockTestHelper
13
14 test 'should be initialized with symbolized client_options' do
15 @options = { :client_options => { 'authorize_url' => 'https://example.com' } }
16 assert_equal 'https://example.com', strategy.client.options[:authorize_url]
17 end
18 end
19
20 module AuthorizeParamsTests
21 extend BlockTestHelper
22
23 test 'should include any authorize params passed in the :authorize_params option' do
24 @options = { :authorize_params => { :foo => 'bar', :baz => 'zip' } }
25 assert_equal 'bar', strategy.authorize_params['foo']
26 assert_equal 'zip', strategy.authorize_params['baz']
27 end
28
29 test 'should include top-level options that are marked as :authorize_options' do
30 @options = { :authorize_options => [:scope, :foo], :scope => 'bar', :foo => 'baz' }
31 assert_equal 'bar', strategy.authorize_params['scope']
32 assert_equal 'baz', strategy.authorize_params['foo']
33 end
34
35 test 'should exclude top-level options that are not passed' do
36 @options = { :authorize_options => [:bar] }
37 refute_has_key :bar, strategy.authorize_params
38 refute_has_key 'bar', strategy.authorize_params
39 end
40 end
41
42 module CSRFAuthorizeParamsTests
43 extend BlockTestHelper
44
45 test 'should store random state in the session when none is present in authorize or request params' do
46 assert_includes strategy.authorize_params.keys, 'state'
47 refute_empty strategy.authorize_params['state']
48 refute_empty strategy.session['omniauth.state']
49 assert_equal strategy.authorize_params['state'], strategy.session['omniauth.state']
50 end
51
52 test 'should store state in the session when present in authorize params vs. a random one' do
53 @options = { :authorize_params => { :state => 'bar' } }
54 refute_empty strategy.authorize_params['state']
55 assert_equal 'bar', strategy.authorize_params[:state]
56 refute_empty strategy.session['omniauth.state']
57 assert_equal 'bar', strategy.session['omniauth.state']
58 end
59
60 test 'should store state in the session when present in request params vs. a random one' do
61 @request.stubs(:params).returns({ 'state' => 'foo' })
62 refute_empty strategy.authorize_params['state']
63 assert_equal 'foo', strategy.authorize_params[:state]
64 refute_empty strategy.session['omniauth.state']
65 assert_equal 'foo', strategy.session['omniauth.state']
66 end
67 end
68
69 module TokenParamsTests
70 extend BlockTestHelper
71
72 test 'should include any authorize params passed in the :token_params option' do
73 @options = { :token_params => { :foo => 'bar', :baz => 'zip' } }
74 assert_equal 'bar', strategy.token_params['foo']
75 assert_equal 'zip', strategy.token_params['baz']
76 end
77
78 test 'should include top-level options that are marked as :token_options' do
79 @options = { :token_options => [:scope, :foo], :scope => 'bar', :foo => 'baz' }
80 assert_equal 'bar', strategy.token_params['scope']
81 assert_equal 'baz', strategy.token_params['foo']
82 end
83 end
84 end
0 require 'helper'
1 require 'omniauth-facebook'
2 require 'openssl'
3 require 'base64'
4
5 class StrategyTest < StrategyTestCase
6 include OAuth2StrategyTests
7 end
8
9 class ClientTest < StrategyTestCase
10 test 'has correct Facebook site' do
11 assert_equal 'https://graph.facebook.com', strategy.client.site
12 end
13
14 test 'has correct authorize url' do
15 assert_equal '/oauth/authorize', strategy.client.options[:authorize_url]
16 end
17
18 test 'has correct token url' do
19 assert_equal '/oauth/access_token', strategy.client.options[:token_url]
20 end
21 end
22
23 class CallbackUrlTest < StrategyTestCase
24 test "returns the default callback url" do
25 url_base = 'http://auth.request.com'
26 @request.stubs(:url).returns("#{url_base}/some/page")
27 strategy.stubs(:script_name).returns('') # as not to depend on Rack env
28 assert_equal "#{url_base}/auth/facebook/callback", strategy.callback_url
29 end
30
31 test "returns path from callback_path option" do
32 @options = { :callback_path => "/auth/FB/done"}
33 url_base = 'http://auth.request.com'
34 @request.stubs(:url).returns("#{url_base}/page/path")
35 strategy.stubs(:script_name).returns('') # as not to depend on Rack env
36 assert_equal "#{url_base}/auth/FB/done", strategy.callback_url
37 end
38
39 test "returns url from callback_url option" do
40 url = 'https://auth.myapp.com/auth/fb/callback'
41 @options = { :callback_url => url }
42 assert_equal url, strategy.callback_url
43 end
44 end
45
46 class AuthorizeParamsTest < StrategyTestCase
47 test 'includes default scope for email' do
48 assert strategy.authorize_params.is_a?(Hash)
49 assert_equal 'email', strategy.authorize_params[:scope]
50 end
51
52 test 'includes display parameter from request when present' do
53 @request.stubs(:params).returns({ 'display' => 'touch' })
54 assert strategy.authorize_params.is_a?(Hash)
55 assert_equal 'touch', strategy.authorize_params[:display]
56 end
57
58 test 'includes state parameter from request when present' do
59 @request.stubs(:params).returns({ 'state' => 'some_state' })
60 assert strategy.authorize_params.is_a?(Hash)
61 assert_equal 'some_state', strategy.authorize_params[:state]
62 end
63
64 test 'overrides default scope with parameter passed from request' do
65 @request.stubs(:params).returns({ 'scope' => 'email' })
66 assert strategy.authorize_params.is_a?(Hash)
67 assert_equal 'email', strategy.authorize_params[:scope]
68 end
69 end
70
71 class TokeParamsTest < StrategyTestCase
72 test 'has correct parse strategy' do
73 assert_equal :query, strategy.token_params[:parse]
74 end
75 end
76
77 class AccessTokenOptionsTest < StrategyTestCase
78 test 'has correct param name by default' do
79 assert_equal 'access_token', strategy.access_token_options[:param_name]
80 end
81
82 test 'has correct header format by default' do
83 assert_equal 'OAuth %s', strategy.access_token_options[:header_format]
84 end
85 end
86
87 class UidTest < StrategyTestCase
88 def setup
89 super
90 strategy.stubs(:raw_info).returns({ 'id' => '123' })
91 end
92
93 test 'returns the id from raw_info' do
94 assert_equal '123', strategy.uid
95 end
96 end
97
98 class InfoTest < StrategyTestCase
99 test 'returns the secure facebook avatar url when `secure_image_url` option is specified' do
100 @options = { :secure_image_url => true }
101 raw_info = { 'name' => 'Fred Smith', 'id' => '321' }
102 strategy.stubs(:raw_info).returns(raw_info)
103 assert_equal 'https://graph.facebook.com/321/picture?type=square', strategy.info['image']
104 end
105
106 test 'returns the image size specified in the `image_size` option' do
107 @options = { :image_size => 'normal' }
108 raw_info = { 'name' => 'Fred Smith', 'id' => '321' }
109 strategy.stubs(:raw_info).returns(raw_info)
110 assert_equal 'http://graph.facebook.com/321/picture?type=normal', strategy.info['image']
111 end
112 end
113
114 class InfoTestOptionalDataPresent < StrategyTestCase
115 def setup
116 super
117 @raw_info ||= { 'name' => 'Fred Smith' }
118 strategy.stubs(:raw_info).returns(@raw_info)
119 end
120
121 test 'returns the name' do
122 assert_equal 'Fred Smith', strategy.info['name']
123 end
124
125 test 'returns the email' do
126 @raw_info['email'] = 'fred@smith.com'
127 assert_equal 'fred@smith.com', strategy.info['email']
128 end
129
130 test 'returns the username as nickname' do
131 @raw_info['username'] = 'fredsmith'
132 assert_equal 'fredsmith', strategy.info['nickname']
133 end
134
135 test 'returns the first name' do
136 @raw_info['first_name'] = 'Fred'
137 assert_equal 'Fred', strategy.info['first_name']
138 end
139
140 test 'returns the last name' do
141 @raw_info['last_name'] = 'Smith'
142 assert_equal 'Smith', strategy.info['last_name']
143 end
144
145 test 'returns the location name as location' do
146 @raw_info['location'] = { 'id' => '104022926303756', 'name' => 'Palo Alto, California' }
147 assert_equal 'Palo Alto, California', strategy.info['location']
148 end
149
150 test 'returns bio as description' do
151 @raw_info['bio'] = 'I am great'
152 assert_equal 'I am great', strategy.info['description']
153 end
154
155 test 'returns the square format facebook avatar url' do
156 @raw_info['id'] = '321'
157 assert_equal 'http://graph.facebook.com/321/picture?type=square', strategy.info['image']
158 end
159
160 test 'returns the Facebook link as the Facebook url' do
161 @raw_info['link'] = 'http://www.facebook.com/fredsmith'
162 assert_kind_of Hash, strategy.info['urls']
163 assert_equal 'http://www.facebook.com/fredsmith', strategy.info['urls']['Facebook']
164 end
165
166 test 'returns website url' do
167 @raw_info['website'] = 'https://my-wonderful-site.com'
168 assert_kind_of Hash, strategy.info['urls']
169 assert_equal 'https://my-wonderful-site.com', strategy.info['urls']['Website']
170 end
171
172 test 'return both Facebook link and website urls' do
173 @raw_info['link'] = 'http://www.facebook.com/fredsmith'
174 @raw_info['website'] = 'https://my-wonderful-site.com'
175 assert_kind_of Hash, strategy.info['urls']
176 assert_equal 'http://www.facebook.com/fredsmith', strategy.info['urls']['Facebook']
177 assert_equal 'https://my-wonderful-site.com', strategy.info['urls']['Website']
178 end
179
180 test 'returns the positive verified status' do
181 @raw_info['verified'] = true
182 assert strategy.info['verified']
183 end
184
185 test 'returns the negative verified status' do
186 @raw_info['verified'] = false
187 refute strategy.info['verified']
188 end
189 end
190
191 class InfoTestOptionalDataNotPresent < StrategyTestCase
192 def setup
193 super
194 @raw_info ||= { 'name' => 'Fred Smith' }
195 strategy.stubs(:raw_info).returns(@raw_info)
196 end
197
198 test 'has no email key' do
199 refute_has_key 'email', strategy.info
200 end
201
202 test 'has no nickname key' do
203 refute_has_key 'nickname', strategy.info
204 end
205
206 test 'has no first name key' do
207 refute_has_key 'first_name', strategy.info
208 end
209
210 test 'has no last name key' do
211 refute_has_key 'last_name', strategy.info
212 end
213
214 test 'has no location key' do
215 refute_has_key 'location', strategy.info
216 end
217
218 test 'has no description key' do
219 refute_has_key 'description', strategy.info
220 end
221
222 test 'has no urls' do
223 refute_has_key 'urls', strategy.info
224 end
225
226 test 'has no verified key' do
227 refute_has_key 'verified', strategy.info
228 end
229 end
230
231 class RawInfoTest < StrategyTestCase
232 def setup
233 super
234 @access_token = stub('OAuth2::AccessToken')
235 end
236
237 test 'performs a GET to https://graph.facebook.com/me' do
238 strategy.stubs(:access_token).returns(@access_token)
239 @access_token.expects(:get).with('/me').returns(stub_everything('OAuth2::Response'))
240 strategy.raw_info
241 end
242
243 test 'returns a Hash' do
244 strategy.stubs(:access_token).returns(@access_token)
245 raw_response = stub('Faraday::Response')
246 raw_response.stubs(:body).returns('{ "ohai": "thar" }')
247 raw_response.stubs(:status).returns(200)
248 raw_response.stubs(:headers).returns({'Content-Type' => 'application/json' })
249 oauth2_response = OAuth2::Response.new(raw_response)
250 @access_token.stubs(:get).with('/me').returns(oauth2_response)
251 assert_kind_of Hash, strategy.raw_info
252 assert_equal 'thar', strategy.raw_info['ohai']
253 end
254
255 test 'returns an empty hash when the response is false' do
256 strategy.stubs(:access_token).returns(@access_token)
257 oauth2_response = stub('OAuth2::Response', :parsed => false)
258 @access_token.stubs(:get).with('/me').returns(oauth2_response)
259 assert_kind_of Hash, strategy.raw_info
260 end
261
262 test 'should not include raw_info in extras hash when skip_info is specified' do
263 @options = { :skip_info => true }
264 strategy.stubs(:raw_info).returns({:foo => 'bar' })
265 refute_has_key 'raw_info', strategy.extra
266 end
267 end
268
269 class CredentialsTest < StrategyTestCase
270 def setup
271 super
272 @access_token = stub('OAuth2::AccessToken')
273 @access_token.stubs(:token)
274 @access_token.stubs(:expires?)
275 @access_token.stubs(:expires_at)
276 @access_token.stubs(:refresh_token)
277 strategy.stubs(:access_token).returns(@access_token)
278 end
279
280 test 'returns a Hash' do
281 assert_kind_of Hash, strategy.credentials
282 end
283
284 test 'returns the token' do
285 @access_token.stubs(:token).returns('123')
286 assert_equal '123', strategy.credentials['token']
287 end
288
289 test 'returns the expiry status' do
290 @access_token.stubs(:expires?).returns(true)
291 assert strategy.credentials['expires']
292
293 @access_token.stubs(:expires?).returns(false)
294 refute strategy.credentials['expires']
295 end
296
297 test 'returns the refresh token and expiry time when expiring' do
298 ten_mins_from_now = (Time.now + 600).to_i
299 @access_token.stubs(:expires?).returns(true)
300 @access_token.stubs(:refresh_token).returns('321')
301 @access_token.stubs(:expires_at).returns(ten_mins_from_now)
302 assert_equal '321', strategy.credentials['refresh_token']
303 assert_equal ten_mins_from_now, strategy.credentials['expires_at']
304 end
305
306 test 'does not return the refresh token when test is nil and expiring' do
307 @access_token.stubs(:expires?).returns(true)
308 @access_token.stubs(:refresh_token).returns(nil)
309 assert_nil strategy.credentials['refresh_token']
310 refute_has_key 'refresh_token', strategy.credentials
311 end
312
313 test 'does not return the refresh token when not expiring' do
314 @access_token.stubs(:expires?).returns(false)
315 @access_token.stubs(:refresh_token).returns('XXX')
316 assert_nil strategy.credentials['refresh_token']
317 refute_has_key 'refresh_token', strategy.credentials
318 end
319 end
320
321 class ExtraTest < StrategyTestCase
322 def setup
323 super
324 @raw_info = { 'name' => 'Fred Smith' }
325 strategy.stubs(:raw_info).returns(@raw_info)
326 end
327
328 test 'returns a Hash' do
329 assert_kind_of Hash, strategy.extra
330 end
331
332 test 'contains raw info' do
333 assert_equal({ 'raw_info' => @raw_info }, strategy.extra)
334 end
335 end
336
337 module SignedRequestHelpers
338 def signed_request(payload, secret)
339 encoded_payload = base64_encode_url(MultiJson.encode(payload))
340 encoded_signature = base64_encode_url(signature(encoded_payload, secret))
341 [encoded_signature, encoded_payload].join('.')
342 end
343
344 def base64_encode_url(value)
345 Base64.encode64(value).tr('+/', '-_').gsub(/\n/, '')
346 end
347
348 def signature(payload, secret, algorithm = OpenSSL::Digest::SHA256.new)
349 OpenSSL::HMAC.digest(algorithm, secret, payload)
350 end
351 end
352
353 module SignedRequestTests
354 class TestCase < StrategyTestCase
355 include SignedRequestHelpers
356 end
357
358 class CookieAndParamNotPresentTest < TestCase
359 test 'is nil' do
360 assert_nil strategy.send(:signed_request)
361 end
362 end
363
364 class CookiePresentTest < TestCase
365 def setup
366 super
367 @payload = {
368 'algorithm' => 'HMAC-SHA256',
369 'code' => 'm4c0d3z',
370 'issued_at' => Time.now.to_i,
371 'user_id' => '123456'
372 }
373
374 @request.stubs(:cookies).returns({"fbsr_#{@client_id}" => signed_request(@payload, @client_secret)})
375 end
376
377 test 'parses the access code out from the cookie' do
378 assert_equal @payload, strategy.send(:signed_request)
379 end
380 end
381
382 class ParamPresentTest < TestCase
383 def setup
384 super
385 @payload = {
386 'algorithm' => 'HMAC-SHA256',
387 'oauth_token' => 'XXX',
388 'issued_at' => Time.now.to_i,
389 'user_id' => '123456'
390 }
391
392 @request.stubs(:params).returns({'signed_request' => signed_request(@payload, @client_secret)})
393 end
394
395 test 'parses the access code out from the param' do
396 assert_equal @payload, strategy.send(:signed_request)
397 end
398 end
399
400 class CookieAndParamPresentTest < TestCase
401 def setup
402 super
403 @payload_from_cookie = {
404 'algorithm' => 'HMAC-SHA256',
405 'from' => 'cookie'
406 }
407
408 @request.stubs(:cookies).returns({"fbsr_#{@client_id}" => signed_request(@payload_from_cookie, @client_secret)})
409
410 @payload_from_param = {
411 'algorithm' => 'HMAC-SHA256',
412 'from' => 'param'
413 }
414
415 @request.stubs(:params).returns({'signed_request' => signed_request(@payload_from_param, @client_secret)})
416 end
417
418 test 'picks param over cookie' do
419 assert_equal @payload_from_param, strategy.send(:signed_request)
420 end
421 end
422 end
423
424 class RequestPhaseWithSignedRequestTest < StrategyTestCase
425 include SignedRequestHelpers
426
427 def setup
428 super
429
430 payload = {
431 'algorithm' => 'HMAC-SHA256',
432 'oauth_token' => 'm4c0d3z'
433 }
434 @raw_signed_request = signed_request(payload, @client_secret)
435 @request.stubs(:params).returns("signed_request" => @raw_signed_request)
436
437 strategy.stubs(:callback_url).returns('/')
438 end
439
440 test 'redirects to callback passing along signed request' do
441 strategy.expects(:redirect).with("/?signed_request=#{Rack::Utils.escape(@raw_signed_request)}").once
442 strategy.request_phase
443 end
444 end
445
446 module BuildAccessTokenTests
447 class TestCase < StrategyTestCase
448 include SignedRequestHelpers
449 end
450
451 class ParamsContainSignedRequestWithAccessTokenTest < TestCase
452 def setup
453 super
454
455 @payload = {
456 'algorithm' => 'HMAC-SHA256',
457 'oauth_token' => 'm4c0d3z',
458 'expires' => Time.now.to_i
459 }
460 @raw_signed_request = signed_request(@payload, @client_secret)
461 @request.stubs(:params).returns({"signed_request" => @raw_signed_request})
462
463 strategy.stubs(:callback_url).returns('/')
464 end
465
466 test 'returns a new access token from the signed request' do
467 result = strategy.build_access_token
468 assert_kind_of ::OAuth2::AccessToken, result
469 assert_equal @payload['oauth_token'], result.token
470 end
471
472 test 'returns an access token with the correct expiry time' do
473 result = strategy.build_access_token
474 assert_equal @payload['expires'], result.expires_at
475 end
476 end
477
478 class ParamsContainAccessTokenStringTest < TestCase
479 def setup
480 super
481
482 @request.stubs(:params).returns({'access_token' => 'm4c0d3z'})
483
484 strategy.stubs(:callback_url).returns('/')
485 end
486
487 test 'returns a new access token' do
488 result = strategy.build_access_token
489 assert_kind_of ::OAuth2::AccessToken, result
490 assert_equal 'm4c0d3z', result.token
491 end
492 end
493 end