add support for facebook canvas
Mark Dodwell
12 years ago
35 | 35 | * `display`: The display context to show the authentication page. Options are: `page`, `popup`, `iframe`, `touch` and `wap`. Read the Facebook docs for more details: http://developers.facebook.com/docs/reference/dialogs#display. Default: `page` |
36 | 36 | |
37 | 37 | For example, to request `email`, `offline_access` and `read_stream` permissions and display the authentication page in a popup window: |
38 | ||
38 | ||
39 | 39 | ```ruby |
40 | 40 | Rails.application.config.middleware.use OmniAuth::Builder do |
41 | 41 | provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'], |
6 | 6 | GEM |
7 | 7 | remote: http://rubygems.org/ |
8 | 8 | specs: |
9 | addressable (2.2.6) | |
10 | faraday (0.7.5) | |
11 | addressable (~> 2.2.6) | |
12 | multipart-post (~> 1.1.3) | |
13 | rack (>= 1.1.0, < 2) | |
9 | addressable (2.2.7) | |
10 | faraday (0.7.6) | |
11 | addressable (~> 2.2) | |
12 | multipart-post (~> 1.1) | |
13 | rack (~> 1.1) | |
14 | 14 | hashie (1.2.0) |
15 | multi_json (1.0.4) | |
16 | multipart-post (1.1.4) | |
15 | multi_json (1.1.0) | |
16 | multipart-post (1.1.5) | |
17 | 17 | oauth2 (0.5.2) |
18 | 18 | faraday (~> 0.7) |
19 | 19 | multi_json (~> 1.0) |
20 | omniauth (1.0.1) | |
20 | omniauth (1.0.2) | |
21 | 21 | hashie (~> 1.2) |
22 | 22 | rack |
23 | 23 | omniauth-oauth2 (1.0.0) |
1 | 1 | require 'sinatra/base' |
2 | 2 | require 'omniauth-facebook' |
3 | 3 | |
4 | # https://github.com/intridea/omniauth-oauth2/pull/9 | |
5 | require 'timeout' | |
6 | ||
4 | 7 | SCOPE = 'email,read_stream' |
5 | 8 | |
6 | 9 | class App < Sinatra::Base |
10 | # turn off sinatra default X-Frame-Options for FB canvas | |
11 | set :protection, :except => :frame_options | |
12 | ||
7 | 13 | # server-side flow |
8 | 14 | get '/' do |
9 | 15 | # 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 | |
16 | # in a real app. the redirect is just here to setup the root | |
11 | 17 | # path in this example sinatra app. |
12 | 18 | redirect '/auth/facebook' |
13 | 19 | end |
14 | ||
20 | ||
15 | 21 | # client-side flow |
16 | 22 | get '/client-side' do |
17 | 23 | content_type 'text/html' |
46 | 52 | js.src = "//connect.facebook.net/en_US/all.js"; |
47 | 53 | d.getElementsByTagName('head')[0].appendChild(js); |
48 | 54 | }(document)); |
49 | ||
55 | ||
50 | 56 | $(function() { |
51 | 57 | $('a').click(function(e) { |
52 | 58 | e.preventDefault(); |
53 | ||
59 | ||
54 | 60 | FB.login(function(response) { |
55 | 61 | if (response.authResponse) { |
56 | 62 | $('#connect').html('Connected! Hitting OmniAuth callback (GET /auth/facebook/callback)...'); |
57 | ||
58 | // since we have cookies enabled, this request will allow omniauth to parse | |
63 | ||
64 | // since we have cookies enabled, this request will allow omniauth to parse | |
59 | 65 | // out the auth code from the signed request in the fbsr_XXX cookie |
60 | 66 | $.getJSON('/auth/facebook/callback', function(json) { |
61 | 67 | $('#connect').html('Connected! Callback complete.'); |
66 | 72 | }); |
67 | 73 | }); |
68 | 74 | </script> |
69 | ||
75 | ||
70 | 76 | <p id="connect"> |
71 | 77 | <a href="#">Connect to FB</a> |
72 | 78 | </p> |
73 | ||
79 | ||
74 | 80 | <p id="results" /> |
75 | 81 | </body> |
76 | 82 | </html> |
77 | 83 | END |
78 | 84 | end |
79 | 85 | |
86 | # auth via FB canvas and signed request param | |
87 | post '/canvas/' do | |
88 | # we just redirect to /auth/facebook here which will parse the | |
89 | # signed_request FB sends us, asking for auth if the user has | |
90 | # not already granted access, or simply moving straight to the | |
91 | # callback where they have already granted access. | |
92 | # | |
93 | # we pass the state parameter which we detect in our callback | |
94 | # to do custom rendering/redirection for the canvas app page | |
95 | redirect "/auth/facebook?signed_request=#{request.params['signed_request']}&state=canvas" | |
96 | end | |
97 | ||
80 | 98 | get '/auth/:provider/callback' do |
99 | # we can do something special here is +state+ param is canvas | |
100 | # (see notes abovein /canvas/ method for more details) | |
81 | 101 | content_type 'application/json' |
82 | 102 | MultiJson.encode(request.env) |
83 | 103 | end |
84 | ||
104 | ||
85 | 105 | get '/auth/failure' do |
86 | 106 | content_type 'application/json' |
87 | 107 | MultiJson.encode(request.env) |
0 | 0 | require 'omniauth/strategies/oauth2' |
1 | 1 | require 'base64' |
2 | 2 | require 'openssl' |
3 | require 'rack/utils' | |
3 | 4 | |
4 | 5 | module OmniAuth |
5 | 6 | module Strategies |
6 | 7 | class Facebook < OmniAuth::Strategies::OAuth2 |
8 | class NoAuthorizationCodeError < StandardError; end | |
9 | ||
7 | 10 | DEFAULT_SCOPE = 'email,offline_access' |
8 | ||
11 | ||
9 | 12 | option :client_options, { |
10 | 13 | :site => 'https://graph.facebook.com', |
11 | 14 | :token_url => '/oauth/access_token' |
19 | 22 | :header_format => 'OAuth %s', |
20 | 23 | :param_name => 'access_token' |
21 | 24 | } |
22 | ||
25 | ||
23 | 26 | option :authorize_options, [:scope, :display] |
24 | ||
27 | ||
25 | 28 | uid { raw_info['id'] } |
26 | ||
29 | ||
27 | 30 | info do |
28 | 31 | prune!({ |
29 | 32 | 'nickname' => raw_info['username'], |
40 | 43 | 'location' => (raw_info['location'] || {})['name'] |
41 | 44 | }) |
42 | 45 | end |
43 | ||
46 | ||
44 | 47 | credentials do |
45 | 48 | prune!({ |
46 | 49 | 'expires' => access_token.expires?, |
47 | 50 | 'expires_at' => access_token.expires_at |
48 | 51 | }) |
49 | 52 | end |
50 | ||
53 | ||
51 | 54 | extra do |
52 | 55 | prune!({ |
53 | 56 | 'raw_info' => raw_info |
54 | 57 | }) |
55 | 58 | end |
56 | ||
59 | ||
57 | 60 | def raw_info |
58 | 61 | @raw_info ||= access_token.get('/me').parsed |
59 | 62 | end |
60 | 63 | |
61 | 64 | def build_access_token |
62 | with_authorization_code { super }.tap do |token| | |
63 | token.options.merge!(access_token_options) | |
64 | end | |
65 | end | |
66 | ||
67 | # NOTE if we're using code from the signed request cookie | |
65 | if signed_request_contains_access_token? | |
66 | hash = signed_request.clone | |
67 | ::OAuth2::AccessToken.new( | |
68 | client, | |
69 | hash.delete('oauth_token'), | |
70 | hash.merge!(access_token_options) | |
71 | ) | |
72 | else | |
73 | with_authorization_code! { super }.tap do |token| | |
74 | token.options.merge!(access_token_options) | |
75 | end | |
76 | end | |
77 | end | |
78 | ||
79 | def request_phase | |
80 | if signed_request_contains_access_token? | |
81 | # if we already have an access token, we can just hit the | |
82 | # callback URL directly and pass the signed request along | |
83 | params = { :signed_request => raw_signed_request } | |
84 | params[:state] = request.params['state'] if request.params['state'] | |
85 | query = Rack::Utils.build_query(params) | |
86 | ||
87 | url = callback_url | |
88 | url << "?" unless url.match(/\?/) | |
89 | url << "&" unless url.match(/[\&\?]$/) | |
90 | url << query | |
91 | ||
92 | redirect url | |
93 | else | |
94 | super | |
95 | end | |
96 | end | |
97 | ||
98 | # NOTE if we're using code from the signed request | |
68 | 99 | # then FB sets the redirect_uri to '' during the authorize |
69 | 100 | # phase + it must match during the access_token phase: |
70 | 101 | # https://github.com/facebook/php-sdk/blob/master/src/base_facebook.php#L348 |
71 | 102 | def callback_url |
72 | if @authorization_code_from_cookie | |
103 | if @authorization_code_from_signed_request | |
73 | 104 | '' |
74 | 105 | else |
75 | 106 | options[:callback_url] || super |
79 | 110 | def access_token_options |
80 | 111 | options.access_token_options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h } |
81 | 112 | end |
82 | ||
113 | ||
83 | 114 | ## |
84 | 115 | # You can pass +display+, +state+ or +scope+ params to the auth request, if |
85 | 116 | # you need to set them dynamically. You can also set these options |
89 | 120 | # |
90 | 121 | def authorize_params |
91 | 122 | super.tap do |params| |
92 | params.merge!(:display => request.params['display']) if request.params['display'] | |
93 | params.merge!(:state => request.params['state']) if request.params['state'] | |
94 | params.merge!(:scope => request.params['scope']) if request.params['scope'] | |
123 | %w[display state scope].each { |v| params[v.to_sym] = request.params[v] if request.params[v] } | |
95 | 124 | params[:scope] ||= DEFAULT_SCOPE |
96 | 125 | end |
97 | 126 | end |
98 | 127 | |
128 | ## | |
129 | # Parse signed request in order, from: | |
130 | # | |
131 | # 1. the request 'signed_request' param (server-side flow from canvas pages) or | |
132 | # 2. a cookie (client-side flow via JS SDK) | |
133 | # | |
99 | 134 | def signed_request |
100 | @signed_request ||= begin | |
101 | cookie = request.cookies["fbsr_#{client.id}"] and | |
102 | parse_signed_request(cookie) | |
103 | end | |
104 | end | |
105 | ||
135 | @signed_request ||= raw_signed_request && | |
136 | parse_signed_request(raw_signed_request) | |
137 | end | |
138 | ||
106 | 139 | private |
107 | ||
108 | # picks the authorization code in order, from: | |
109 | # 1. the request param | |
110 | # 2. a signed cookie | |
111 | def with_authorization_code | |
140 | ||
141 | def raw_signed_request | |
142 | request.params['signed_request'] || | |
143 | request.cookies["fbsr_#{client.id}"] | |
144 | end | |
145 | ||
146 | def signed_request_contains_access_token? | |
147 | signed_request && | |
148 | signed_request['oauth_token'] | |
149 | end | |
150 | ||
151 | ## | |
152 | # Picks the authorization code in order, from: | |
153 | # | |
154 | # 1. the request 'code' param (manual callback from standard server-side flow) | |
155 | # 2. a signed request (see #signed_request for more) | |
156 | # | |
157 | def with_authorization_code! | |
112 | 158 | if request.params.key?('code') |
113 | 159 | yield |
114 | else code_from_cookie = signed_request && signed_request['code'] | |
115 | request.params['code'] = code_from_cookie | |
116 | @authorization_code_from_cookie = true | |
160 | elsif code_from_signed_request = signed_request && signed_request['code'] | |
161 | request.params['code'] = code_from_signed_request | |
162 | @authorization_code_from_signed_request = true | |
117 | 163 | begin |
118 | 164 | yield |
119 | 165 | ensure |
120 | 166 | request.params.delete('code') |
121 | @authorization_code_from_cookie = false | |
167 | @authorization_code_from_signed_request = false | |
122 | 168 | end |
123 | end | |
124 | end | |
125 | ||
169 | else | |
170 | raise NoAuthorizationCodeError, 'must pass either a `code` parameter or a signed request (via `signed_request` parameter or a `fbsr_XXX` cookie)' | |
171 | end | |
172 | end | |
173 | ||
126 | 174 | def prune!(hash) |
127 | hash.delete_if do |_, value| | |
175 | hash.delete_if do |_, value| | |
128 | 176 | prune!(value) if value.is_a?(Hash) |
129 | 177 | value.nil? || (value.respond_to?(:empty?) && value.empty?) |
130 | 178 | end |
131 | 179 | end |
132 | ||
180 | ||
133 | 181 | def parse_signed_request(value) |
134 | 182 | signature, encoded_payload = value.split('.') |
135 | 183 |
7 | 7 | @request = double('Request') |
8 | 8 | @request.stub(:params) { {} } |
9 | 9 | @request.stub(:cookies) { {} } |
10 | ||
10 | ||
11 | 11 | @client_id = '123' |
12 | 12 | @client_secret = '53cr3tz' |
13 | 13 | end |
14 | ||
14 | ||
15 | 15 | subject do |
16 | 16 | args = [@client_id, @client_secret, @options].compact |
17 | 17 | OmniAuth::Strategies::Facebook.new(nil, *args).tap do |strategy| |
42 | 42 | subject.stub(:script_name) { '' } # as not to depend on Rack env |
43 | 43 | subject.callback_url.should eq("#{url_base}/auth/facebook/callback") |
44 | 44 | end |
45 | ||
45 | ||
46 | 46 | it "returns path from callback_path option" do |
47 | 47 | @options = { :callback_path => "/auth/FB/done"} |
48 | 48 | url_base = 'http://auth.request.com' |
50 | 50 | subject.stub(:script_name) { '' } # as not to depend on Rack env |
51 | 51 | subject.callback_url.should eq("#{url_base}/auth/FB/done") |
52 | 52 | end |
53 | ||
53 | ||
54 | 54 | it "returns url from callback_url option" do |
55 | 55 | url = 'https://auth.myapp.com/auth/fb/callback' |
56 | 56 | @options = { :callback_url => url } |
63 | 63 | subject.authorize_params.should be_a(Hash) |
64 | 64 | subject.authorize_params[:scope].should eq('email,offline_access') |
65 | 65 | end |
66 | ||
66 | ||
67 | 67 | it 'includes display parameter from request when present' do |
68 | 68 | @request.stub(:params) { { 'display' => 'touch' } } |
69 | 69 | subject.authorize_params.should be_a(Hash) |
98 | 98 | subject.access_token_options[:header_format].should eq('OAuth %s') |
99 | 99 | end |
100 | 100 | end |
101 | ||
101 | ||
102 | 102 | describe '#uid' do |
103 | 103 | before :each do |
104 | 104 | subject.stub(:raw_info) { { 'id' => '123' } } |
105 | 105 | end |
106 | ||
106 | ||
107 | 107 | it 'returns the id from raw_info' do |
108 | 108 | subject.uid.should eq('123') |
109 | 109 | end |
110 | 110 | end |
111 | ||
111 | ||
112 | 112 | describe '#info' do |
113 | 113 | before :each do |
114 | 114 | @raw_info ||= { 'name' => 'Fred Smith' } |
115 | 115 | subject.stub(:raw_info) { @raw_info } |
116 | 116 | end |
117 | ||
117 | ||
118 | 118 | context 'when optional data is not present in raw info' do |
119 | 119 | it 'has no email key' do |
120 | 120 | subject.info.should_not have_key('email') |
123 | 123 | it 'has no nickname key' do |
124 | 124 | subject.info.should_not have_key('nickname') |
125 | 125 | end |
126 | ||
126 | ||
127 | 127 | it 'has no first name key' do |
128 | 128 | subject.info.should_not have_key('first_name') |
129 | 129 | end |
130 | ||
130 | ||
131 | 131 | it 'has no last name key' do |
132 | 132 | subject.info.should_not have_key('last_name') |
133 | 133 | end |
134 | ||
134 | ||
135 | 135 | it 'has no location key' do |
136 | 136 | subject.info.should_not have_key('location') |
137 | 137 | end |
138 | ||
138 | ||
139 | 139 | it 'has no description key' do |
140 | 140 | subject.info.should_not have_key('description') |
141 | 141 | end |
142 | ||
142 | ||
143 | 143 | it 'has no urls' do |
144 | 144 | subject.info.should_not have_key('urls') |
145 | 145 | end |
146 | 146 | end |
147 | ||
147 | ||
148 | 148 | context 'when data is present in raw info' do |
149 | 149 | it 'returns the name' do |
150 | 150 | subject.info['name'].should eq('Fred Smith') |
151 | 151 | end |
152 | ||
152 | ||
153 | 153 | it 'returns the email' do |
154 | 154 | @raw_info['email'] = 'fred@smith.com' |
155 | 155 | subject.info['email'].should eq('fred@smith.com') |
159 | 159 | @raw_info['username'] = 'fredsmith' |
160 | 160 | subject.info['nickname'].should eq('fredsmith') |
161 | 161 | end |
162 | ||
162 | ||
163 | 163 | it 'returns the first name' do |
164 | 164 | @raw_info['first_name'] = 'Fred' |
165 | 165 | subject.info['first_name'].should eq('Fred') |
166 | 166 | end |
167 | ||
167 | ||
168 | 168 | it 'returns the last name' do |
169 | 169 | @raw_info['last_name'] = 'Smith' |
170 | 170 | subject.info['last_name'].should eq('Smith') |
171 | 171 | end |
172 | ||
172 | ||
173 | 173 | it 'returns the location name as location' do |
174 | 174 | @raw_info['location'] = { 'id' => '104022926303756', 'name' => 'Palo Alto, California' } |
175 | 175 | subject.info['location'].should eq('Palo Alto, California') |
176 | 176 | end |
177 | ||
177 | ||
178 | 178 | it 'returns bio as description' do |
179 | 179 | @raw_info['bio'] = 'I am great' |
180 | 180 | subject.info['description'].should eq('I am great') |
181 | 181 | end |
182 | ||
182 | ||
183 | 183 | it 'returns the square format facebook avatar url' do |
184 | 184 | @raw_info['id'] = '321' |
185 | 185 | subject.info['image'].should eq('http://graph.facebook.com/321/picture?type=square') |
186 | 186 | end |
187 | ||
187 | ||
188 | 188 | it 'returns the Facebook link as the Facebook url' do |
189 | 189 | @raw_info['link'] = 'http://www.facebook.com/fredsmith' |
190 | 190 | subject.info['urls'].should be_a(Hash) |
191 | 191 | subject.info['urls']['Facebook'].should eq('http://www.facebook.com/fredsmith') |
192 | 192 | end |
193 | ||
193 | ||
194 | 194 | it 'returns website url' do |
195 | 195 | @raw_info['website'] = 'https://my-wonderful-site.com' |
196 | 196 | subject.info['urls'].should be_a(Hash) |
197 | 197 | subject.info['urls']['Website'].should eq('https://my-wonderful-site.com') |
198 | 198 | end |
199 | ||
199 | ||
200 | 200 | it 'return both Facebook link and website urls' do |
201 | 201 | @raw_info['link'] = 'http://www.facebook.com/fredsmith' |
202 | 202 | @raw_info['website'] = 'https://my-wonderful-site.com' |
206 | 206 | end |
207 | 207 | end |
208 | 208 | end |
209 | ||
209 | ||
210 | 210 | describe '#raw_info' do |
211 | 211 | before :each do |
212 | 212 | @access_token = double('OAuth2::AccessToken') |
213 | 213 | subject.stub(:access_token) { @access_token } |
214 | 214 | end |
215 | ||
215 | ||
216 | 216 | it 'performs a GET to https://graph.facebook.com/me' do |
217 | 217 | @access_token.stub(:get) { double('OAuth2::Response').as_null_object } |
218 | 218 | @access_token.should_receive(:get).with('/me') |
219 | 219 | subject.raw_info |
220 | 220 | end |
221 | ||
221 | ||
222 | 222 | it 'returns a Hash' do |
223 | 223 | @access_token.stub(:get).with('/me') do |
224 | 224 | raw_response = double('Faraday::Response') |
241 | 241 | @access_token.stub(:refresh_token) |
242 | 242 | subject.stub(:access_token) { @access_token } |
243 | 243 | end |
244 | ||
244 | ||
245 | 245 | it 'returns a Hash' do |
246 | 246 | subject.credentials.should be_a(Hash) |
247 | 247 | end |
248 | ||
248 | ||
249 | 249 | it 'returns the token' do |
250 | 250 | @access_token.stub(:token) { '123' } |
251 | 251 | subject.credentials['token'].should eq('123') |
252 | 252 | end |
253 | ||
253 | ||
254 | 254 | it 'returns the expiry status' do |
255 | 255 | @access_token.stub(:expires?) { true } |
256 | 256 | subject.credentials['expires'].should eq(true) |
257 | ||
257 | ||
258 | 258 | @access_token.stub(:expires?) { false } |
259 | 259 | subject.credentials['expires'].should eq(false) |
260 | 260 | end |
261 | ||
261 | ||
262 | 262 | it 'returns the refresh token and expiry time when expiring' do |
263 | 263 | ten_mins_from_now = (Time.now + 600).to_i |
264 | 264 | @access_token.stub(:expires?) { true } |
267 | 267 | subject.credentials['refresh_token'].should eq('321') |
268 | 268 | subject.credentials['expires_at'].should eq(ten_mins_from_now) |
269 | 269 | end |
270 | ||
270 | ||
271 | 271 | it 'does not return the refresh token when it is nil and expiring' do |
272 | 272 | @access_token.stub(:expires?) { true } |
273 | 273 | @access_token.stub(:refresh_token) { nil } |
274 | 274 | subject.credentials['refresh_token'].should be_nil |
275 | 275 | subject.credentials.should_not have_key('refresh_token') |
276 | 276 | end |
277 | ||
277 | ||
278 | 278 | it 'does not return the refresh token when not expiring' do |
279 | 279 | @access_token.stub(:expires?) { false } |
280 | 280 | @access_token.stub(:refresh_token) { 'XXX' } |
282 | 282 | subject.credentials.should_not have_key('refresh_token') |
283 | 283 | end |
284 | 284 | end |
285 | ||
285 | ||
286 | 286 | describe '#extra' do |
287 | 287 | before :each do |
288 | 288 | @raw_info = { 'name' => 'Fred Smith' } |
289 | 289 | subject.stub(:raw_info) { @raw_info } |
290 | 290 | end |
291 | ||
291 | ||
292 | 292 | it 'returns a Hash' do |
293 | 293 | subject.extra.should be_a(Hash) |
294 | 294 | end |
295 | ||
295 | ||
296 | 296 | it 'contains raw info' do |
297 | 297 | subject.extra.should eq({ 'raw_info' => @raw_info }) |
298 | 298 | end |
299 | 299 | end |
300 | 300 | |
301 | 301 | describe '#signed_request' do |
302 | context 'cookie not present' do | |
302 | context 'cookie/param not present' do | |
303 | 303 | it 'is nil' do |
304 | 304 | subject.send(:signed_request).should be_nil |
305 | 305 | end |
306 | 306 | end |
307 | ||
307 | ||
308 | 308 | context 'cookie present' do |
309 | 309 | before :each do |
310 | 310 | @payload = { |
321 | 321 | |
322 | 322 | it 'parses the access code out from the cookie' do |
323 | 323 | subject.send(:signed_request).should eq(@payload) |
324 | end | |
325 | end | |
326 | ||
327 | context 'param present' do | |
328 | before :each do | |
329 | @payload = { | |
330 | 'algorithm' => 'HMAC-SHA256', | |
331 | 'oauth_token' => 'XXX', | |
332 | 'issued_at' => Time.now.to_i, | |
333 | 'user_id' => '123456' | |
334 | } | |
335 | ||
336 | @request.stub(:params) do | |
337 | { 'signed_request' => signed_request(@payload, @client_secret) } | |
338 | end | |
339 | end | |
340 | ||
341 | it 'parses the access code out from the param' do | |
342 | subject.send(:signed_request).should eq(@payload) | |
343 | end | |
344 | end | |
345 | ||
346 | context 'cookie + param present' do | |
347 | before :each do | |
348 | @payload_from_cookie = { | |
349 | 'algorithm' => 'HMAC-SHA256', | |
350 | 'from' => 'cookie' | |
351 | } | |
352 | ||
353 | @request.stub(:cookies) do | |
354 | { "fbsr_#{@client_id}" => signed_request(@payload_from_cookie, @client_secret) } | |
355 | end | |
356 | ||
357 | @payload_from_param = { | |
358 | 'algorithm' => 'HMAC-SHA256', | |
359 | 'from' => 'param' | |
360 | } | |
361 | ||
362 | @request.stub(:params) do | |
363 | { 'signed_request' => signed_request(@payload_from_param, @client_secret) } | |
364 | end | |
365 | end | |
366 | ||
367 | it 'picks param over cookie' do | |
368 | subject.send(:signed_request).should eq(@payload_from_param) | |
369 | end | |
370 | end | |
371 | end | |
372 | ||
373 | describe '#request_phase' do | |
374 | describe 'params contain a signed request with an access token' do | |
375 | before do | |
376 | payload = { | |
377 | 'algorithm' => 'HMAC-SHA256', | |
378 | 'oauth_token' => 'm4c0d3z' | |
379 | } | |
380 | @raw_signed_request = signed_request(payload, @client_secret) | |
381 | @request.stub(:params) do | |
382 | { "signed_request" => @raw_signed_request } | |
383 | end | |
384 | ||
385 | subject.stub(:callback_url) { '/' } | |
386 | end | |
387 | ||
388 | it 'redirects to callback passing along signed request' do | |
389 | subject.should_receive(:redirect).with("/?signed_request=#{Rack::Utils.escape(@raw_signed_request)}").once | |
390 | subject.request_phase | |
391 | end | |
392 | end | |
393 | end | |
394 | ||
395 | describe '#build_access_token' do | |
396 | describe 'params contain a signed request with an access token' do | |
397 | before do | |
398 | @payload = { | |
399 | 'algorithm' => 'HMAC-SHA256', | |
400 | 'oauth_token' => 'm4c0d3z' | |
401 | } | |
402 | @raw_signed_request = signed_request(@payload, @client_secret) | |
403 | @request.stub(:params) do | |
404 | { "signed_request" => @raw_signed_request } | |
405 | end | |
406 | ||
407 | subject.stub(:callback_url) { '/' } | |
408 | end | |
409 | ||
410 | it 'returns a new access token from the signed request' do | |
411 | result = subject.build_access_token | |
412 | result.should be_an_instance_of(::OAuth2::AccessToken) | |
413 | result.token.should eq(@payload['oauth_token']) | |
324 | 414 | end |
325 | 415 | end |
326 | 416 | end |
337 | 427 | Base64.encode64(value).tr('+/', '-_').gsub(/\n/, '') |
338 | 428 | end |
339 | 429 | |
340 | def signature(payload, secret, algorithm = OpenSSL::Digest::SHA256.new) | |
430 | def signature(payload, secret, algorithm = OpenSSL::Digest::SHA256.new) | |
341 | 431 | OpenSSL::HMAC.digest(algorithm, secret, payload) |
342 | 432 | end |
343 | 433 | end |