fbauth 0.9.9.9 → 1.0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.mdown CHANGED
@@ -1,54 +1,239 @@
1
- FBAuth
2
- ======
1
+ # FBAuth #
3
2
 
4
- This gem provides authentication and basic Facebook functions for your Rails application.
3
+ This gem provides authentication and basic Facebook functions for your
4
+ Rails application.
5
5
 
6
- The Authentication Challenge
7
- ----------------------------
6
+ ## The Authentication Challenge ##
8
7
 
9
- Facebook is an evolving platform, over the past couple years we've seen a lot of change in how it authenticates users of
10
- third-party applications.
8
+ Facebook is an evolving platform, over the past couple years we've seen
9
+ a lot of change in how it authenticates users of third-party
10
+ applications.
11
11
 
12
- And as of this writing, authentication with the Javascript SDK remains broken due to the reliance on cross-domain
13
- cookies, which are simply not supported in mobile Safari, by default on Safari for Windows, and reportedly on the
14
- Android Webkit based browser.
12
+ The documentation for Facebook is minimal, without historic reference or
13
+ in-depth discussion of the different implementation scenarios that our
14
+ applications may take.
15
15
 
16
- This plugin uses a few techniques to locate your "access token" and prefers to use the OAuth API to get what you need as
17
- a Facebook app to ensure your users have correctly added your app, authenticated in Facebook, and to communicate with
18
- the Graph API.
16
+ For example, the preferred method of doing "Canvas" applications, those
17
+ that live within the Facebook "wrapper" is now to use an iFrame. If you
18
+ choose to utilize the Javascript Connect SDK and rely on its
19
+ authentication mechanisms this will fail under any browsers that block
20
+ cross-domain cookies - which is more prominent as time goes on due to
21
+ the security risks involved in allowing cross-domain cookies.
22
+
23
+ Therefore, as of this writing, applications using the prescribed iFrame
24
+ style and using the JS SDK will fail on mobile Safari, Safari on
25
+ Windows' default settings, and various mobile implementations of the
26
+ Android browser.
27
+
28
+ This plugin uses a few techniques to locate your "access token" and
29
+ prefers to use the OAuth API to get what you need as a Facebook app to
30
+ ensure your users have correctly added your app, authenticated in
31
+ Facebook, and to communicate with the Graph and Query APIs.
19
32
 
20
33
  Here are the scenarios we currently handle:
21
34
 
22
- iFrame Apps
23
- -----------
35
+ __iFrame Apps__
24
36
 
25
- - first page load as an iFrame app inside Facebook, where authentication params are sent in the URL used for your iFrame
26
- - this is particularly required for mobile Safari and other browsers blocking cross-domain cookies by default
27
- - handles the old session parameter, as well as the new signed_request parameter
37
+ - first page load as an iFrame app inside Facebook, where authentication
38
+ params are sent in the URL used for your iFrame
39
+ - this is particularly required for mobile Safari and other browsers
40
+ blocking cross-domain cookies by default
41
+ - handles the old session parameter, as well as the new signed_request
42
+ parameter
28
43
 
29
44
  - loading from the cookie initialized by the JavaScript API
30
45
  - works great for browsers supporting cross-domain cookies by default
31
46
 
32
- - the access token you get is time-limited, if it has expired you need to be re-authenticated
47
+ - the access token you get is time-limited, if it has expired you need
48
+ to be re-authenticated
49
+
50
+ __External (Connect) Apps__
51
+
52
+ - handling an OAuth exchange back & forth with Facebook to handle
53
+ authentication and capture URL parameters back for token
54
+
55
+ ## Integration with Your Rails App ##
56
+
57
+ Add fbauth to your Gemfile
58
+
59
+ gem 'fbauth', '~> 1.0'
60
+
61
+ Create `config/facebook.yml`
62
+
63
+ development:
64
+ app_id: 'xxxxxxxxxxxxx'
65
+ app_context: 'my-app-dev'
66
+ auth_path: '/login'
67
+ canvas_url: 'http://dev.myapp.com/facebook'
68
+ app_secret: 'xxxxxxxxxxxx'
69
+
70
+ test:
71
+ app_id: 'fake_id'
72
+ app_context: 'my-app'
73
+ auth_path: '/login'
74
+ canvas_url: 'http://myapp.com/facebook'
75
+ app_secret: 'fake_secret'
76
+
77
+ production:
78
+ app_id: 'xxxxxxxxxxxxx'
79
+ app_context: 'my-app'
80
+ auth_path: '/login'
81
+ canvas_url: 'http://myapp.com/facebook'
82
+ app_secret: 'xxxxxxxxxxxx'
83
+
84
+ - `app_id` - this is your Facebook App ID
85
+ - `app_context` - this is your Canvas Page path, ie.
86
+ `http://apps.facebook.com/my-app-dev`
87
+ - `auth_path` - the path in your application to your login page (must be
88
+ a string, not a logical route name)
89
+ - `canvas_url` - this is the Facebook Canvas URL, the base URL that gets
90
+ to the Facebook iFrame pages for your Canvas app
91
+ - `app_secret` - the Facebook App Secret code for your application
92
+
93
+ Note: _We are assuming that you will be registering **two** facebook
94
+ applications, one that you will be using to actively develop your
95
+ application and the other for production use for your users._
96
+
97
+ Include the fbauth modules in your application controllers
98
+
99
+ include FacebookAuthFunctions
100
+ include FbauthHelper
101
+
102
+ In the controllers you want restricted to authorized users, or in your
103
+ `Application` controller, add the filters
104
+
105
+ before_filter :require_facebook_auth
106
+
107
+ Create the controller and method that will live at your `auth_path`
108
+ specified in your facebook.yml file.
109
+
110
+ class AuthController < ApplicationController
111
+
112
+ # Use this if you've included FacebookAuthFunctions in your
113
+ # Application controller...
114
+ # skip_before_filter :require_facebook_auth
115
+
116
+ def login
117
+ end
118
+ end
119
+
120
+ And then create your login template (we use HAML, but you can use ERB if
121
+ you want.
122
+
123
+ %div.fblogin
124
+ You are not logged in to Facebook, please click here:
125
+ %fb:login-button{:perms => 'publish_stream'} Log In to Facebook
126
+
127
+ %div.fbadd
128
+ Please add this Application - it's great!
129
+ %fb:login-button{:perms => 'publish_stream'} Add This Application
130
+
131
+ %div.fbready
132
+ Please wait...
33
133
 
34
- External (Connect) Apps
35
- -----------------------
134
+ = fbauth_login_javascript(:login => '.fblogin', :add => '.fbadd', :ready => '.fbready')
36
135
 
37
- - handling an OAuth exchange back & forth with Facebook to handle authentication and capture URL parameters back for
38
- token
136
+ The three areas noted above are exposed in the following scenarios:
39
137
 
40
- Things Remaining Unclear
41
- ========================
138
+ - `:login` - this element is exposed when the user has not logged into
139
+ Facebook, they may or may not have added your application but we don't
140
+ know that yet.
141
+ - `:add` - this element is exposed when the user is logged in to
142
+ Facebook but has not added / authorized your application. Note we are
143
+ requesting certain permissions when the app is added:
144
+ `<fb:login-button perms="publish_stream"></fb:login-button>`
145
+ - `:ready` - once the user has satisfied the pop-ups that appear when
146
+ logging in and/or adding the application, or if the user loads this
147
+ screen and is already fully authenticated, this element appears and
148
+ a redirect is done to your application root_path
42
149
 
43
- Documentation for the Facebook platform is a little fragmented, so we haven't (that we recall) come across the answers
44
- to these questions yet:
150
+ ## Using the Facebook APIs ##
45
151
 
46
- - what timezone is the OAuth token expiry value in? (we get it in Epoch, no TZ data, currently assuming San Francisco)
152
+ ### Graph API ###
153
+
154
+ The Graph API gives you the ability to request a great deal of
155
+ information about individual social relationships and perform basic
156
+ updates to that graph (ie. posting news-feed events).
157
+
158
+ Certain actions (posting changes, reading certain restricted attributes)
159
+ are only available if you provide an access token and make an
160
+ authenticated call.
161
+
162
+ __Unauthenticated Call__
163
+
164
+ graph = FacebookGraph.new
165
+ graph.call('svetzal')
166
+
167
+ This will retrieve publicly available information on the provided user's
168
+ Facebook UID or handle in a Ruby hash structure.
169
+
170
+ {"name"=>"Steven Vetzal", "gender"=>"male", "id"=>"849395216", "last_name"=>"Vetzal",
171
+ "locale"=>"en_US", "link"=>"http://www.facebook.com/svetzal", "first_name"=>"Steven"}
172
+
173
+ __Authenticated Call__
174
+
175
+ graph = FacebookGraph.new(fbauth.access_token)
176
+ graph.call('svetzal', { :scope => "birthday" })
177
+
178
+ This will make a similar call but as an authenticated caller we can
179
+ request a restricted scope, in this case information about the user's
180
+ birthday (if our app has requested the birthday permission from the
181
+ user).
182
+
183
+ ### Query API (FQL) ###
184
+
185
+ __Unauthenticated Call__
186
+
187
+ query = FacebookQuery.new
188
+ query.fql('SELECT name, first_name, last_name FROM user WHERE uid in ("849395216","58001611","1018515154")')
189
+
190
+ FQL is handy for performing multiple lookups at once, and can save you a
191
+ lot of latency.
192
+
193
+ [
194
+ {"name"=>"Steven Vetzal", "last_name"=>"Vetzal", "first_name"=>"Steven"},
195
+ {"name"=>"Nate Smith", "last_name"=>"Smith", "first_name"=>"Nate"},
196
+ {"name"=>"Craig Savolainen", "last_name"=>"Savolainen", "first_name"=>"Craig"}
197
+ ]
198
+
199
+ __Authenticated Call__
200
+
201
+ query = FacebookQuery.new(fbauth.access_token)
202
+
203
+ __Manually Building an Access Token__
204
+
205
+ If you scan your logs, you'll see the `signed_request` OAuth GET
206
+ parameter being sent to your application. You can manually build
207
+ yourself an authentication token from this using the following code.
208
+
209
+ data = FacebookDecoder.decode('{"signed_request"=>"oXWWpLi2tX7QW3cjX...(removed)"}')
210
+ fbauth = FacebookAuth.create(data)
211
+ fbauth.validate
212
+
213
+ The `.validate` step is optional, but pre-warns you if the token you are
214
+ about to use is good. If it is not, you can find out why by looking at
215
+ `fbauth.validation_error`.
216
+
217
+ # Things Remaining Unclear #
218
+
219
+ Documentation for the Facebook platform is a little fragmented, so we
220
+ haven't (that we recall) come across the answers to these questions yet:
221
+
222
+ - what timezone is the OAuth token expiry value in? (we get it in Epoch,
223
+ no TZ data, currently assuming San Francisco)
47
224
  - what happens when time approaches the OAuth token expiry?
48
225
  - do we get a new one?
49
226
  - are we expected to stop functioning and redirect to a FB login?
50
227
 
51
- Change Log
52
- ==========
228
+ # Change Log #
229
+
230
+ v1.0.0.0
231
+
232
+ - Changed call semantics for FacebookGraph and FacebookQuery,
233
+ use objects instead of class methods
234
+ - Preparing for public release
53
235
 
54
- v0.9.9.6 - Raising info-rich exceptions when errors returned from Facebook on graph calls
236
+ v0.9.9.6
237
+
238
+ - Raising info-rich exceptions when errors returned from
239
+ Facebook on graph calls
@@ -5,9 +5,6 @@ module FacebookAuthFunctions
5
5
  end
6
6
 
7
7
  def require_facebook_auth
8
- # Prep IE so it will take our cookies in a Facebook iFrame
9
- response.headers['P3P'] = 'CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"'
10
-
11
8
  setup_facebook_auth
12
9
  if @facebook_auth.nil?
13
10
  redirect_to build_auth_url
@@ -21,6 +18,9 @@ private
21
18
  end
22
19
 
23
20
  def facebook_auth
21
+ # Prep IE so it will take our cookies in a Facebook iFrame
22
+ response.headers['P3P'] = 'CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"'
23
+
24
24
  # If we have valid auth in session, use it
25
25
  data = parse_session
26
26
  auth = validate_and_save(data) unless data.nil?
@@ -70,6 +70,7 @@ private
70
70
  parms = JSON.parse(params[:session])
71
71
  logger.warn("Parsed facebook params from session parameter (deprecated)")
72
72
  elsif params[:signed_request].present?
73
+ logger.warn("Found signed_request param")
73
74
  begin
74
75
  parms = FacebookDecoder.decode(params[:signed_request])
75
76
  logger.warn("Parsed facebook params from signed_request parameter")
data/lib/facebook_auth.rb CHANGED
@@ -36,7 +36,7 @@ class FacebookAuth
36
36
  msgs = []
37
37
  unless self.uid.nil? || self.access_token.nil?
38
38
  begin
39
- self.user_data = FacebookGraph.call(self.uid, self.access_token)
39
+ self.user_data = FacebookGraph.new(self.access_token).call(self.uid)
40
40
  rescue => e
41
41
  msgs << "Error calling FacebookGraph - #{e}"
42
42
  end
@@ -2,56 +2,25 @@ require 'net/http'
2
2
  require 'uri'
3
3
 
4
4
  class FacebookGraph
5
+ include FacebookHttp
5
6
 
6
- def self.call(path, access_token = nil, options = {})
7
- params = []
8
- options.keys.each do |k|
9
- params << "#{k}=#{options[k]}" unless options[k].nil?
10
- end
11
- token = access_token ? CGI::escape(access_token) : nil
12
- url = "https://graph.facebook.com/#{path}&access_token=#{token}"
13
- url = "#{url}?#{params.join('&')}" unless params.empty?
14
- uri = URI.parse(url)
15
- http = Net::HTTP.new uri.host, uri.port
16
- begin
17
- http.use_ssl = (uri.scheme == "https")
18
- req = Net::HTTP::Get.new(uri.path)
19
- response = http.request(req)
20
- raise "Facebook error response #{response.code} - #{response.body}" unless response.code == '200'
21
- begin
22
- json = JSON.parse(response.body)
23
- rescue => e
24
- raise "Error parsing Facebook response: #{response.body}"
25
- end
26
- ensure
27
- http.finish if http.started?
28
- end
29
- json
7
+ FB_GRAPH_URL = "https://graph.facebook.com"
8
+
9
+ def initialize(access_token = nil, options = {})
10
+ @options = options.merge({ :access_token => access_token })
11
+ end
12
+
13
+ # Generic Graph call to lookup data for any path
14
+ def call(path, options = {})
15
+ get "#{FB_GRAPH_URL}/#{path}", merged_options(options)
30
16
  end
31
17
 
32
- # Available options:
33
- # message, picture, link, name, caption, description
34
- def self.publish_to_member_feed(uid, access_token, options)
35
- token = access_token ? CGI::escape(access_token) : nil
36
- if %w{staging production}.include? ENV['RAILS_ENV']
37
- url = "https://graph.facebook.com/#{uid}/feed&access_token=#{token}"
38
- uri = URI.parse(url)
39
- http = Net::HTTP.new uri.host, uri.port
40
- begin
41
- http.use_ssl = (uri.scheme == "https")
42
- req = Net::HTTP::Post.new(uri.path)
43
- req.set_form_data(options)
44
- response = http.request(req)
45
- raise "Facebook error response #{response.code} - #{response.body}" unless response.code == '200'
46
- begin
47
- json = JSON.parse(response.body)
48
- rescue => e
49
- raise "Error parsing Facebook response: #{response.body}"
50
- end
51
- ensure
52
- http.finish if http.started?
53
- end
54
- json
18
+ # Post item to member's wall
19
+ # Available options: message, picture, link, name, caption, description
20
+ def publish_to_member_feed(uid, options)
21
+ raise "access_token required" unless has_access_token?(options)
22
+ if %w{staging production}.include? Rails.env
23
+ post "#{FB_GRAPH_URL}/#{uid}/feed", merged_options(options)
55
24
  end
56
25
  end
57
26
  end
@@ -0,0 +1,87 @@
1
+ require 'benchmark'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require 'cgi'
5
+
6
+ module FacebookHttp
7
+
8
+ def build_get_url(url, params = {})
9
+ q = build_query_string(params)
10
+ if q
11
+ url + q
12
+ else
13
+ url
14
+ end
15
+ end
16
+
17
+ def get(url, params = {})
18
+ json = nil
19
+ uri = URI.parse(build_get_url(url, params))
20
+ bench = Benchmark.measure do
21
+ http = Net::HTTP.new uri.host, uri.port
22
+ begin
23
+ http.use_ssl = (uri.scheme == "https")
24
+ req = Net::HTTP::Get.new(uri.request_uri)
25
+ response = http.request(req)
26
+ raise "Facebook error response #{response.code} - #{response.body}" unless response.code == '200'
27
+ begin
28
+ json = JSON.parse(response.body)
29
+ rescue => e
30
+ raise "Error parsing facebook response: #{response.body}"
31
+ end
32
+ ensure
33
+ http.finish if http.started?
34
+ end
35
+ end
36
+ logger.warn("Facebook GET call to #{uri.to_s} completed in #{bench.total} seconds")
37
+ json
38
+ end
39
+
40
+ def post(url, params = {})
41
+ json = nil
42
+ uri = URI.parse(url)
43
+ bench = Benchmark.measure do
44
+ http = Net::HTTP.new uri.host, uri.port
45
+ begin
46
+ http.use_ssl = (uri.scheme == "https")
47
+ req = Net::HTTP::Post.new(uri.path)
48
+ req.set_form_data(params)
49
+ response = http.request(req)
50
+ raise "Facebook error response #{response.code} - #{response.body}" unless response.code == '200'
51
+ begin
52
+ json = JSON.parse(response.body)
53
+ rescue => e
54
+ raise "Error parsing Facebook response: #{response.body}"
55
+ end
56
+ ensure
57
+ http.finish if http.started?
58
+ end
59
+ end
60
+ logger.warn("Facebook POST call to #{uri.to_s} completed in #{bench.total} seconds")
61
+ json
62
+ end
63
+
64
+ def build_query_string options={}
65
+ params = []
66
+ str_keys = options.keys.collect{ |k| k.to_s }
67
+ str_keys.sort.each do |str_key|
68
+ key = str_key.to_sym
69
+ value = options[key]
70
+ params << "#{key.to_s}=#{URI.escape(value.to_s)}" unless value.nil?
71
+ end
72
+ "?#{params.join('&')}" unless params.empty?
73
+ end
74
+
75
+ def merged_options(options = {})
76
+ options.merge!(@options) if @options
77
+ options
78
+ end
79
+
80
+ def has_access_token?(options = {})
81
+ merged_options.has_key? :access_token
82
+ end
83
+
84
+ def logger
85
+ Rails.logger
86
+ end
87
+ end
@@ -0,0 +1,16 @@
1
+ require 'uri'
2
+
3
+ class FacebookQuery
4
+ include FacebookHttp
5
+
6
+ FB_API_URL = "https://api.facebook.com/method/fql.query"
7
+
8
+ def initialize(access_token = nil, options = {})
9
+ @options = options.merge({ :access_token => access_token })
10
+ @options.merge!({ :format => "JSON" }) unless @options.has_key?(:format)
11
+ end
12
+
13
+ def fql(query, options = {})
14
+ get FB_API_URL, merged_options(options.merge({ :query => query }))
15
+ end
16
+ end
data/lib/fbauth.rb CHANGED
@@ -12,4 +12,6 @@ end
12
12
  require 'facebook_decoder.rb'
13
13
  require 'facebook_auth.rb'
14
14
  require 'facebook_config.rb'
15
+ require 'facebook_http.rb'
15
16
  require 'facebook_graph.rb'
17
+ require 'facebook_query.rb'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fbauth
3
3
  version: !ruby/object:Gem::Version
4
- hash: 49
4
+ hash: 95
5
5
  prerelease: false
6
6
  segments:
7
+ - 1
7
8
  - 0
8
- - 9
9
- - 9
10
- - 9
11
- version: 0.9.9.9
9
+ - 0
10
+ - 0
11
+ version: 1.0.0.0
12
12
  platform: ruby
13
13
  authors:
14
14
  - Three Wise Men Inc.
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2011-02-28 00:00:00 -05:00
19
+ date: 2011-03-03 00:00:00 -05:00
20
20
  default_executable:
21
21
  dependencies: []
22
22
 
@@ -33,6 +33,8 @@ files:
33
33
  - lib/facebook_config.rb
34
34
  - lib/facebook_decoder.rb
35
35
  - lib/facebook_graph.rb
36
+ - lib/facebook_http.rb
37
+ - lib/facebook_query.rb
36
38
  - lib/fbauth.rb
37
39
  - app/controllers/facebook_auth_functions.rb
38
40
  - app/helpers/fbauth_helper.rb