Codebase list ruby-omniauth-facebook / 972ed5e
Merge branch 'client_side_flow_support_via_signed_cookie' [closes #10] Mark Dodwell 12 years ago
5 changed file(s) with 207 addition(s) and 19 deletion(s). Raw diff Collapse all Expand all
00 source :rubygems
11
22 gemspec
3
4 gem 'jruby-openssl', :platform => :jruby
11
22 This gem contains the Facebook strategy for OmniAuth 1.0.
33
4 Supports the OAuth 2.0 server-side flow. Read the Facebook docs for more details: http://developers.facebook.com/docs/authentication
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
55
66 ## Installing
77
2424 provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
2525 end
2626 ```
27
28 See a full example of both server and client-side flows in the example Sinatra app in the `example/` folder above.
2729
2830 ## Configuring
2931
8789
8890 The precise information available may depend on the permissions which you request.
8991
92 ## Client-side Flow
93
94 The client-side flow supports parsing the authorization code from the signed request which Facebook puts into a cookie. This means you can to use the Facebook Javascript SDK as you would normally, and you just hit the callback endpoint (`/auth/facebook/callback` by default) once the user has authenticated in the `FB.login` success callback.
95
96 See the example Sinatra app under `example/` for more details.
97
9098 ## Supported Rubies
9199
92100 Actively tested with the following Ruby versions:
95103 - MRI 1.9.2
96104 - MRI 1.8.7
97105 - JRuby 1.6.5
106
107 *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:
108
109 ```ruby
110 gem 'jruby-openssl', :platform => :jruby
111 ```
98112
99113 ## License
100114
11 require 'sinatra/base'
22 require 'omniauth-facebook'
33
4 SCOPE = 'email,read_stream'
5
46 class App < Sinatra::Base
7 # server-side flow
58 get '/' do
9 # NOTE: you would just hit this endpoint directly from the browser
10 # in a real app. the redirect is just here to setup the root
11 # path in this example sinatra app.
612 redirect '/auth/facebook'
13 end
14
15 # client-side flow
16 get '/client-side' do
17 content_type 'text/html'
18 # NOTE: when you enable cookie below in the FB.init call
19 # the GET request in the FB.login callback will send
20 # a signed request in a cookie back the OmniAuth callback
21 # which will parse out the authorization code and obtain
22 # the access_token. This will be the exact same access_token
23 # returned to the client in response.authResponse.accessToken.
24 <<-END
25 <html>
26 <head>
27 <title>Client-side Flow Example</title>
28 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js" type="text/javascript"></script>
29 </head>
30 <body>
31 <div id="fb-root"></div>
32
33 <script type="text/javascript">
34 window.fbAsyncInit = function() {
35 FB.init({
36 appId : '#{ENV['APP_ID']}',
37 status : true, // check login status
38 cookie : true, // enable cookies to allow the server to access the session
39 oauth : true, // enable OAuth 2.0
40 xfbml : true // parse XFBML
41 });
42 };
43
44 (function(d) {
45 var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {return;}
46 js = d.createElement('script'); js.id = id; js.async = true;
47 js.src = "//connect.facebook.net/en_US/all.js";
48 d.getElementsByTagName('head')[0].appendChild(js);
49 }(document));
50
51 $(function() {
52 $('a').click(function(e) {
53 e.preventDefault();
54
55 FB.login(function(response) {
56 if (response.authResponse) {
57 $.get('/auth/facebook/callback');
58 }
59 }, { scope: '#{SCOPE}' });
60 });
61 });
62 </script>
63
64 <p>
65 <a href="#">Connect to FB</a>
66 </p>
67 </body>
68 </html>
69 END
770 end
871
972 get '/auth/:provider/callback' do
2083 use Rack::Session::Cookie
2184
2285 use OmniAuth::Builder do
23 provider :facebook, ENV['APP_ID'], ENV['APP_SECRET'], :scope => 'email,read_stream', :display => 'popup'
86 provider :facebook, ENV['APP_ID'], ENV['APP_SECRET'], :scope => SCOPE
2487 end
2588
2689 run App.new
00 require 'omniauth/strategies/oauth2'
1 require 'base64'
2 require 'openssl'
13
24 module OmniAuth
35 module Strategies
5658 @raw_info ||= access_token.get('/me').parsed
5759 end
5860
59 def callback_url
60 if options.authorize_options.respond_to? :callback_url
61 options.authorize_options.callback_url
62 else
63 super
61 def build_access_token
62 with_authorization_code { super }.tap do |token|
63 token.options.merge!(access_token_options)
6464 end
6565 end
66
67 def build_access_token
68 super.tap do |token|
69 token.options.merge!(access_token_options)
66
67 # NOTE if we're using code from the signed request cookie
68 # then FB sets the redirect_uri to '' during the authorize
69 # phase + it must match during the access_token phase:
70 # https://github.com/facebook/php-sdk/blob/master/src/base_facebook.php#L348
71 def callback_url
72 if @authorization_code_from_cookie
73 ''
74 else
75 if options.authorize_options.respond_to?(:callback_url)
76 options.authorize_options.callback_url
77 else
78 super
79 end
7080 end
7181 end
7282
8090 params[:scope] ||= DEFAULT_SCOPE
8191 end
8292 end
93
94 def signed_request
95 @signed_request ||= begin
96 cookie = request.cookies["fbsr_#{client.id}"] and
97 parse_signed_request(cookie)
98 end
99 end
83100
84101 private
102
103 # picks the authorization code in order, from:
104 # 1. the request param
105 # 2. a signed cookie
106 def with_authorization_code
107 if request.params.key?('code')
108 yield
109 else code_from_cookie = signed_request && signed_request['code']
110 request.params['code'] = code_from_cookie
111 @authorization_code_from_cookie = true
112 begin
113 yield
114 ensure
115 request.params.delete('code')
116 @authorization_code_from_cookie = false
117 end
118 end
119 end
85120
86121 def prune!(hash)
87122 hash.delete_if do |_, value|
89124 value.nil? || (value.respond_to?(:empty?) && value.empty?)
90125 end
91126 end
127
128 def parse_signed_request(value)
129 signature, encoded_payload = value.split('.')
130
131 decoded_hex_signature = base64_decode_url(signature)#.unpack('H*')
132 decoded_payload = MultiJson.decode(base64_decode_url(encoded_payload))
133
134 unless decoded_payload['algorithm'] == 'HMAC-SHA256'
135 raise NotImplementedError, "unkown algorithm: #{decoded_payload['algorithm']}"
136 end
137
138 if valid_signature?(client.secret, decoded_hex_signature, encoded_payload)
139 decoded_payload
140 end
141 end
142
143 def valid_signature?(secret, signature, payload, algorithm = OpenSSL::Digest::SHA256.new)
144 OpenSSL::HMAC.digest(algorithm, secret, payload) == signature
145 end
146
147 def base64_decode_url(value)
148 value += '=' * (4 - value.size.modulo(4))
149 Base64.decode64(value.tr('-_', '+/'))
150 end
92151 end
93152 end
94153 end
00 require 'spec_helper'
11 require 'omniauth-facebook'
2 require 'openssl'
3 require 'base64'
24
35 describe OmniAuth::Strategies::Facebook do
46 before :each do
57 @request = double('Request')
68 @request.stub(:params) { {} }
9 @request.stub(:cookies) { {} }
10
11 @client_id = '123'
12 @client_secret = '53cr3tz'
713 end
814
915 subject do
10 OmniAuth::Strategies::Facebook.new(nil, @options || {}).tap do |strategy|
16 args = [@client_id, @client_secret, @options].compact
17 OmniAuth::Strategies::Facebook.new(nil, *args).tap do |strategy|
1118 strategy.stub(:request) { @request }
1219 end
1320 end
3138 describe '#callback_url' do
3239 it "returns value from #authorize_options" do
3340 url = 'http://auth.myapp.com/auth/fb/callback'
34 @options = {:authorize_options => { :callback_url => url }}
35 subject.callback_url.should == url
36 end
37
38 it " callback_url from request" do
41 @options = { :authorize_options => { :callback_url => url } }
42 subject.callback_url.should eq(url)
43 end
44
45 it "callback_url from request" do
3946 url_base = 'http://auth.request.com'
40 @request.stub(:url){ url_base + "/page/path" }
47 @request.stub(:url) { "#{url_base}/page/path" }
4148 subject.stub(:script_name) { "" } # to not depend from Rack env
42 subject.callback_url.should == url_base + "/auth/facebook/callback"
49 subject.callback_url.should eq("#{url_base}/auth/facebook/callback")
4350 end
4451 end
4552
270277 subject.extra.should eq({ 'raw_info' => @raw_info })
271278 end
272279 end
280
281 describe '#signed_request' do
282 context 'cookie not present' do
283 it 'is nil' do
284 subject.send(:signed_request).should be_nil
285 end
286 end
287
288 context 'cookie present' do
289 before :each do
290 @payload = {
291 'algorithm' => 'HMAC-SHA256',
292 'code' => 'm4c0d3z',
293 'issued_at' => Time.now.to_i,
294 'user_id' => '123456'
295 }
296
297 @request.stub(:cookies) do
298 { "fbsr_#{@client_id}" => signed_request(@payload, @client_secret) }
299 end
300 end
301
302 it 'parses the access code out from the cookie' do
303 subject.send(:signed_request).should eq(@payload)
304 end
305 end
306 end
307
308 private
309
310 def signed_request(payload, secret)
311 encoded_payload = base64_encode_url(MultiJson.encode(payload))
312 encoded_signature = base64_encode_url(signature(encoded_payload, secret))
313 [encoded_signature, encoded_payload].join('.')
314 end
315
316 def base64_encode_url(value)
317 Base64.encode64(value).tr('+/', '-_').gsub(/\n/, '')
318 end
319
320 def signature(payload, secret, algorithm = OpenSSL::Digest::SHA256.new)
321 OpenSSL::HMAC.digest(algorithm, secret, payload)
322 end
273323 end