songkick-oauth2-provider 0.10.2 → 0.10.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/History.txt +7 -0
- data/README.rdoc +18 -11
- data/example/README.rdoc +1 -1
- data/example/application.rb +9 -9
- data/example/schema.rb +1 -1
- data/example/views/authorize.erb +2 -2
- data/example/views/layout.erb +4 -4
- data/example/views/login.erb +2 -2
- data/example/views/new_client.erb +1 -1
- data/example/views/new_user.erb +1 -1
- data/lib/songkick/oauth2/model.rb +8 -6
- data/lib/songkick/oauth2/model/authorization.rb +31 -31
- data/lib/songkick/oauth2/model/client.rb +15 -15
- data/lib/songkick/oauth2/model/client_owner.rb +2 -2
- data/lib/songkick/oauth2/model/hashing.rb +3 -3
- data/lib/songkick/oauth2/model/helpers.rb +16 -0
- data/lib/songkick/oauth2/model/resource_owner.rb +4 -4
- data/lib/songkick/oauth2/provider.rb +16 -16
- data/lib/songkick/oauth2/provider/access_token.rb +20 -15
- data/lib/songkick/oauth2/provider/authorization.rb +43 -42
- data/lib/songkick/oauth2/provider/error.rb +4 -4
- data/lib/songkick/oauth2/provider/exchange.rb +46 -46
- data/lib/songkick/oauth2/router.rb +13 -13
- data/lib/songkick/oauth2/schema.rb +11 -3
- data/lib/songkick/oauth2/schema/20120828112156_songkick_oauth2_schema_original_schema.rb +2 -2
- data/lib/songkick/oauth2/schema/20121024180930_songkick_oauth2_schema_add_authorization_index.rb +3 -3
- data/lib/songkick/oauth2/schema/20121025180447_songkick_oauth2_schema_add_unique_indexes.rb +7 -7
- data/spec/request_helpers.rb +25 -21
- data/spec/songkick/oauth2/model/authorization_spec.rb +56 -56
- data/spec/songkick/oauth2/model/client_spec.rb +9 -9
- data/spec/songkick/oauth2/model/helpers_spec.rb +26 -0
- data/spec/songkick/oauth2/model/resource_owner_spec.rb +13 -13
- data/spec/songkick/oauth2/provider/access_token_spec.rb +32 -20
- data/spec/songkick/oauth2/provider/authorization_spec.rb +73 -62
- data/spec/songkick/oauth2/provider/exchange_spec.rb +72 -72
- data/spec/songkick/oauth2/provider_spec.rb +101 -101
- data/spec/spec_helper.rb +5 -3
- data/spec/test_app/helper.rb +11 -7
- data/spec/test_app/provider/application.rb +12 -12
- data/spec/test_app/provider/views/authorize.erb +2 -2
- metadata +71 -93
@@ -8,91 +8,91 @@ describe Songkick::OAuth2::Provider::Exchange do
|
|
8
8
|
@authorization = create_authorization(:client => @client, :owner => @bob, :code => 'a_fake_code', :scope => 'foo bar')
|
9
9
|
Songkick::OAuth2.stub(:random_string).and_return('random_string')
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
let(:exchange) { Songkick::OAuth2::Provider::Exchange.new(@bob, params) }
|
13
|
-
|
13
|
+
|
14
14
|
shared_examples_for "validates required parameters" do
|
15
15
|
describe "missing grant_type" do
|
16
16
|
before { params.delete('client_id') }
|
17
|
-
|
17
|
+
|
18
18
|
it "is invalid" do
|
19
19
|
exchange.error.should == "invalid_request"
|
20
20
|
exchange.error_description.should == "Missing required parameter client_id"
|
21
21
|
end
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
describe "with an unknown grant type" do
|
25
25
|
before { params['grant_type'] = 'unknown' }
|
26
|
-
|
26
|
+
|
27
27
|
it "is invalid" do
|
28
28
|
exchange.error.should == "unsupported_grant_type"
|
29
29
|
exchange.error_description.should == "The grant type unknown is not recognized"
|
30
30
|
end
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
describe "missing client_id" do
|
34
34
|
before { params.delete('client_id') }
|
35
|
-
|
35
|
+
|
36
36
|
it "is invalid" do
|
37
37
|
exchange.error.should == "invalid_request"
|
38
38
|
exchange.error_description.should == "Missing required parameter client_id"
|
39
39
|
end
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
describe "with an unknown client_id" do
|
43
43
|
before { params['client_id'] = "unknown" }
|
44
|
-
|
44
|
+
|
45
45
|
it "is invalid" do
|
46
46
|
exchange.error.should == "invalid_client"
|
47
47
|
exchange.error_description.should == "Unknown client ID unknown"
|
48
48
|
end
|
49
49
|
end
|
50
|
-
|
50
|
+
|
51
51
|
describe "missing client_secret" do
|
52
52
|
before { params.delete('client_secret') }
|
53
|
-
|
53
|
+
|
54
54
|
it "is invalid" do
|
55
55
|
exchange.error.should == "invalid_request"
|
56
56
|
exchange.error_description.should == "Missing required parameter client_secret"
|
57
57
|
end
|
58
58
|
end
|
59
|
-
|
59
|
+
|
60
60
|
describe "with a mismatched client_secret" do
|
61
61
|
before { params['client_secret'] = "nosoupforyou" }
|
62
|
-
|
62
|
+
|
63
63
|
it "is invalid" do
|
64
64
|
exchange.error.should == "invalid_client"
|
65
65
|
exchange.error_description.should == "Parameter client_secret does not match"
|
66
66
|
end
|
67
67
|
end
|
68
|
-
|
68
|
+
|
69
69
|
describe "with lesser scope than the authorization code represents" do
|
70
70
|
before { params['scope'] = 'bar' }
|
71
|
-
|
71
|
+
|
72
72
|
it "is valid" do
|
73
73
|
exchange.error.should be_nil
|
74
74
|
end
|
75
75
|
end
|
76
|
-
|
76
|
+
|
77
77
|
describe "with scopes not covered by the authorization code" do
|
78
78
|
before { params['scope'] = 'qux' }
|
79
|
-
|
79
|
+
|
80
80
|
it "is invalid" do
|
81
81
|
exchange.error.should == 'invalid_scope'
|
82
82
|
exchange.error_description.should == 'The request scope was never granted by the user'
|
83
83
|
end
|
84
84
|
end
|
85
85
|
end
|
86
|
-
|
86
|
+
|
87
87
|
shared_examples_for "valid token request" do
|
88
88
|
before do
|
89
89
|
Songkick::OAuth2.stub(:random_string).and_return('random_access_token')
|
90
90
|
end
|
91
|
-
|
91
|
+
|
92
92
|
it "is valid" do
|
93
93
|
exchange.error.should be_nil
|
94
94
|
end
|
95
|
-
|
95
|
+
|
96
96
|
it "updates the Authorization with tokens" do
|
97
97
|
exchange.update_authorization
|
98
98
|
authorization.reload
|
@@ -101,7 +101,7 @@ describe Songkick::OAuth2::Provider::Exchange do
|
|
101
101
|
authorization.refresh_token.should be_nil
|
102
102
|
end
|
103
103
|
end
|
104
|
-
|
104
|
+
|
105
105
|
describe "using authorization_code grant type" do
|
106
106
|
let(:params) { { 'client_id' => @client.client_id,
|
107
107
|
'client_secret' => @client.client_secret,
|
@@ -109,73 +109,73 @@ describe Songkick::OAuth2::Provider::Exchange do
|
|
109
109
|
'code' => @authorization.code,
|
110
110
|
'redirect_uri' => @client.redirect_uri }
|
111
111
|
}
|
112
|
-
|
112
|
+
|
113
113
|
let(:authorization) { @authorization }
|
114
|
-
|
114
|
+
|
115
115
|
it_should_behave_like "validates required parameters"
|
116
116
|
it_should_behave_like "valid token request"
|
117
|
-
|
117
|
+
|
118
118
|
describe "missing redirect_uri" do
|
119
119
|
before { params.delete('redirect_uri') }
|
120
|
-
|
120
|
+
|
121
121
|
it "is invalid" do
|
122
122
|
exchange.error.should == "invalid_request"
|
123
123
|
exchange.error_description.should == "Missing required parameter redirect_uri"
|
124
124
|
end
|
125
125
|
end
|
126
|
-
|
126
|
+
|
127
127
|
describe "with a mismatched redirect_uri" do
|
128
128
|
before { params['redirect_uri'] = "http://songkick.com" }
|
129
|
-
|
129
|
+
|
130
130
|
it "is invalid" do
|
131
131
|
exchange.error.should == "redirect_uri_mismatch"
|
132
132
|
exchange.error_description.should == "Parameter redirect_uri does not match registered URI"
|
133
133
|
end
|
134
|
-
|
134
|
+
|
135
135
|
describe "when the client has not registered a redirect_uri" do
|
136
136
|
before { @client.update_attribute(:redirect_uri, nil) }
|
137
|
-
|
137
|
+
|
138
138
|
it "is valid" do
|
139
139
|
exchange.error.should be_nil
|
140
140
|
end
|
141
141
|
end
|
142
142
|
end
|
143
|
-
|
143
|
+
|
144
144
|
describe "missing code" do
|
145
145
|
before { params.delete('code') }
|
146
|
-
|
146
|
+
|
147
147
|
it "is invalid" do
|
148
148
|
exchange.error.should == "invalid_request"
|
149
149
|
exchange.error_description.should == "Missing required parameter code"
|
150
150
|
end
|
151
151
|
end
|
152
|
-
|
152
|
+
|
153
153
|
describe "with an unknown code" do
|
154
154
|
before { params['code'] = "unknown" }
|
155
|
-
|
155
|
+
|
156
156
|
it "is invalid" do
|
157
157
|
exchange.error.should == "invalid_grant"
|
158
158
|
exchange.error_description.should == "The access grant you supplied is invalid"
|
159
159
|
end
|
160
160
|
end
|
161
|
-
|
161
|
+
|
162
162
|
describe "with an expired code" do
|
163
163
|
before { @authorization.update_attribute(:expires_at, 1.day.ago) }
|
164
|
-
|
164
|
+
|
165
165
|
it "is invalid" do
|
166
166
|
exchange.error.should == "invalid_grant"
|
167
167
|
exchange.error_description.should == "The access grant you supplied is invalid"
|
168
168
|
end
|
169
169
|
end
|
170
170
|
end
|
171
|
-
|
171
|
+
|
172
172
|
describe "using password grant type" do
|
173
173
|
let(:params) { { 'client_id' => @client.client_id,
|
174
174
|
'client_secret' => @client.client_secret,
|
175
175
|
'grant_type' => 'password',
|
176
176
|
'password' => 'soldier' }
|
177
177
|
}
|
178
|
-
|
178
|
+
|
179
179
|
before do
|
180
180
|
Songkick::OAuth2::Provider.handle_passwords do |client, username, password, scopes|
|
181
181
|
user = TestApp::User[username]
|
@@ -186,54 +186,54 @@ describe Songkick::OAuth2::Provider::Exchange do
|
|
186
186
|
end
|
187
187
|
end
|
188
188
|
end
|
189
|
-
|
189
|
+
|
190
190
|
describe "for a user with existing authorization" do
|
191
191
|
let(:authorization) { @authorization }
|
192
192
|
before { params['username'] = 'Bob' }
|
193
|
-
|
193
|
+
|
194
194
|
it_should_behave_like "validates required parameters"
|
195
195
|
it_should_behave_like "valid token request"
|
196
|
-
|
196
|
+
|
197
197
|
describe "missing username" do
|
198
198
|
before { params.delete('username') }
|
199
|
-
|
199
|
+
|
200
200
|
it "is invalid" do
|
201
201
|
exchange.error.should == 'invalid_request'
|
202
202
|
exchange.error_description.should == 'Missing required parameter username'
|
203
203
|
end
|
204
204
|
end
|
205
|
-
|
205
|
+
|
206
206
|
describe "missing password" do
|
207
207
|
before { params.delete('password') }
|
208
|
-
|
208
|
+
|
209
209
|
it "is invalid" do
|
210
210
|
exchange.error.should == 'invalid_request'
|
211
211
|
exchange.error_description.should == 'Missing required parameter password'
|
212
212
|
end
|
213
213
|
end
|
214
|
-
|
214
|
+
|
215
215
|
describe "with a bad password" do
|
216
216
|
before { params['password'] = 'bad' }
|
217
|
-
|
217
|
+
|
218
218
|
it "is invalid" do
|
219
219
|
exchange.error.should == 'invalid_grant'
|
220
220
|
exchange.error_description.should == 'The access grant you supplied is invalid'
|
221
221
|
end
|
222
222
|
end
|
223
223
|
end
|
224
|
-
|
224
|
+
|
225
225
|
describe "for a user with no existing authorization" do
|
226
226
|
let(:authorization) { Songkick::OAuth2::Model::Authorization.find_by_oauth2_resource_owner_id(@alice.id) }
|
227
227
|
before { params['username'] = 'Alice' }
|
228
|
-
|
228
|
+
|
229
229
|
it_should_behave_like "validates required parameters"
|
230
230
|
it_should_behave_like "valid token request"
|
231
|
-
|
231
|
+
|
232
232
|
describe "with ungranted but permissible scopes" do
|
233
233
|
before { params['scope'] = 'lol' }
|
234
234
|
it_should_behave_like "validates required parameters"
|
235
235
|
it_should_behave_like "valid token request"
|
236
|
-
|
236
|
+
|
237
237
|
it "sets the scope from the request" do
|
238
238
|
exchange.update_authorization
|
239
239
|
authorization.reload
|
@@ -242,7 +242,7 @@ describe Songkick::OAuth2::Provider::Exchange do
|
|
242
242
|
end
|
243
243
|
end
|
244
244
|
end
|
245
|
-
|
245
|
+
|
246
246
|
describe "using assertion grant type" do
|
247
247
|
let(:params) { { 'client_id' => @client.client_id,
|
248
248
|
'client_secret' => @client.client_secret,
|
@@ -250,75 +250,75 @@ describe Songkick::OAuth2::Provider::Exchange do
|
|
250
250
|
'assertion_type' => 'https://graph.facebook.com/me',
|
251
251
|
'assertion' => 'Bob' }
|
252
252
|
}
|
253
|
-
|
253
|
+
|
254
254
|
let(:authorization) { @authorization }
|
255
|
-
|
255
|
+
|
256
256
|
before do
|
257
257
|
Songkick::OAuth2::Provider.filter_assertions { |client| @client == client }
|
258
|
-
|
258
|
+
|
259
259
|
Songkick::OAuth2::Provider.handle_assertions('https://graph.facebook.com/me') do |client, assertion|
|
260
260
|
user = TestApp::User[assertion]
|
261
261
|
user.grant_access!(client, :scopes => ['foo', 'bar'])
|
262
262
|
end
|
263
263
|
end
|
264
|
-
|
264
|
+
|
265
265
|
after do
|
266
266
|
Songkick::OAuth2::Provider.clear_assertion_handlers!
|
267
267
|
end
|
268
|
-
|
268
|
+
|
269
269
|
it_should_behave_like "validates required parameters"
|
270
270
|
it_should_behave_like "valid token request"
|
271
|
-
|
271
|
+
|
272
272
|
describe "missing assertion_type" do
|
273
273
|
before { params.delete('assertion_type') }
|
274
|
-
|
274
|
+
|
275
275
|
it "is invalid" do
|
276
276
|
exchange.error.should == 'invalid_request'
|
277
277
|
exchange.error_description.should == 'Missing required parameter assertion_type'
|
278
278
|
end
|
279
279
|
end
|
280
|
-
|
280
|
+
|
281
281
|
describe "with a non-URI assertion_type" do
|
282
282
|
before { params['assertion_type'] = 'invalid' }
|
283
|
-
|
283
|
+
|
284
284
|
it "is invalid" do
|
285
285
|
exchange.error.should == 'invalid_request'
|
286
286
|
exchange.error_description.should == 'Parameter assertion_type must be an absolute URI'
|
287
287
|
end
|
288
288
|
end
|
289
|
-
|
289
|
+
|
290
290
|
describe "missing assertion" do
|
291
291
|
before { params.delete('assertion') }
|
292
|
-
|
292
|
+
|
293
293
|
it "is invalid" do
|
294
294
|
exchange.error.should == 'invalid_request'
|
295
295
|
exchange.error_description.should == 'Missing required parameter assertion'
|
296
296
|
end
|
297
297
|
end
|
298
|
-
|
298
|
+
|
299
299
|
describe "with an unrecognized assertion_type" do
|
300
300
|
before { params['assertion_type'] = 'https://oauth.what.com/ohai' }
|
301
|
-
|
301
|
+
|
302
302
|
it "is invalid" do
|
303
303
|
exchange.error.should == 'unauthorized_client'
|
304
304
|
exchange.error_description.should == 'Client cannot use the given assertion type'
|
305
305
|
end
|
306
306
|
end
|
307
|
-
|
307
|
+
|
308
308
|
describe "with a client unauthorized to use the assertion scheme" do
|
309
309
|
before do
|
310
310
|
client = Factory(:client)
|
311
311
|
params['client_id'] = client.client_id
|
312
312
|
params['client_secret'] = client.client_secret
|
313
313
|
end
|
314
|
-
|
314
|
+
|
315
315
|
it "is invalid" do
|
316
316
|
exchange.error.should == 'unauthorized_client'
|
317
317
|
exchange.error_description.should == 'Client cannot use the given assertion type'
|
318
318
|
end
|
319
319
|
end
|
320
320
|
end
|
321
|
-
|
321
|
+
|
322
322
|
describe "using refresh_token grant type" do
|
323
323
|
before do
|
324
324
|
@refresher = create_authorization(:client => @client,
|
@@ -327,27 +327,27 @@ describe Songkick::OAuth2::Provider::Exchange do
|
|
327
327
|
:code => nil,
|
328
328
|
:refresh_token => 'roflscale')
|
329
329
|
end
|
330
|
-
|
330
|
+
|
331
331
|
let(:params) { { 'client_id' => @client.client_id,
|
332
332
|
'client_secret' => @client.client_secret,
|
333
333
|
'grant_type' => 'refresh_token',
|
334
334
|
'refresh_token' => 'roflscale' }
|
335
335
|
}
|
336
|
-
|
336
|
+
|
337
337
|
let(:authorization) { @refresher }
|
338
|
-
|
338
|
+
|
339
339
|
it_should_behave_like "validates required parameters"
|
340
340
|
it_should_behave_like "valid token request"
|
341
|
-
|
341
|
+
|
342
342
|
describe "with unknown refresh_token" do
|
343
343
|
before { params['refresh_token'] = 'woops' }
|
344
|
-
|
344
|
+
|
345
345
|
it "is invalid" do
|
346
346
|
exchange.error.should == "invalid_grant"
|
347
347
|
exchange.error_description.should == "The access grant you supplied is invalid"
|
348
348
|
end
|
349
349
|
end
|
350
|
-
|
350
|
+
|
351
351
|
end
|
352
352
|
end
|
353
353
|
|
@@ -1,21 +1,21 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Songkick::OAuth2::Provider do
|
4
|
-
|
4
|
+
include RequestHelpers
|
5
|
+
|
6
|
+
before(:all) { TestApp::Provider.start(RequestHelpers::SERVER_PORT) }
|
5
7
|
after(:all) { TestApp::Provider.stop }
|
6
|
-
|
8
|
+
|
7
9
|
let(:params) { { 'response_type' => 'code',
|
8
10
|
'client_id' => @client.client_id,
|
9
11
|
'redirect_uri' => @client.redirect_uri }
|
10
12
|
}
|
11
|
-
|
13
|
+
|
12
14
|
before do
|
13
15
|
@client = Factory(:client, :name => 'Test client')
|
14
16
|
@owner = TestApp::User['Bob']
|
15
17
|
end
|
16
|
-
|
17
|
-
include RequestHelpers
|
18
|
-
|
18
|
+
|
19
19
|
describe "access grant request" do
|
20
20
|
shared_examples_for "asks for user permission" do
|
21
21
|
it "creates an authorization" do
|
@@ -23,7 +23,7 @@ describe Songkick::OAuth2::Provider do
|
|
23
23
|
Songkick::OAuth2::Provider::Authorization.should_receive(:new).with(@owner, params, nil).and_return(auth)
|
24
24
|
get(params)
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
it "displays an authorization page" do
|
28
28
|
response = get(params)
|
29
29
|
response.code.to_i.should == 200
|
@@ -31,30 +31,30 @@ describe Songkick::OAuth2::Provider do
|
|
31
31
|
response['Content-Type'].should =~ /text\/html/
|
32
32
|
end
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
describe "with valid parameters" do
|
36
36
|
it_should_behave_like "asks for user permission"
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
describe "for token requests" do
|
40
40
|
before { params['response_type'] = 'token' }
|
41
41
|
it_should_behave_like "asks for user permission"
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
describe "for code_and_token requests" do
|
45
45
|
before { params['response_type'] = 'code_and_token' }
|
46
46
|
it_should_behave_like "asks for user permission"
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
describe "enforcing SSL" do
|
50
50
|
before { Songkick::OAuth2::Provider.enforce_ssl = true }
|
51
|
-
|
51
|
+
|
52
52
|
it "does not allow non-SSL requests" do
|
53
53
|
response = get(params)
|
54
54
|
validate_response(response, 400, 'WAT')
|
55
55
|
end
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
describe "when there is already a pending authorization from the user" do
|
59
59
|
before do
|
60
60
|
@authorization = create_authorization(
|
@@ -63,31 +63,31 @@ describe Songkick::OAuth2::Provider do
|
|
63
63
|
:code => 'pending_code',
|
64
64
|
:scope => 'offline_access')
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
it "immediately redirects with the code" do
|
68
68
|
response = get(params)
|
69
69
|
response.code.to_i.should == 302
|
70
70
|
response['location'].should == 'https://client.example.com/cb?code=pending_code'
|
71
71
|
end
|
72
|
-
|
72
|
+
|
73
73
|
describe "when the client is requesting scopes it already has access to" do
|
74
74
|
before { params['scope'] = 'offline_access' }
|
75
|
-
|
75
|
+
|
76
76
|
it "immediately redirects with the code" do
|
77
77
|
response = get(params)
|
78
78
|
response.code.to_i.should == 302
|
79
79
|
response['location'].should == 'https://client.example.com/cb?code=pending_code&scope=offline_access'
|
80
80
|
end
|
81
81
|
end
|
82
|
-
|
82
|
+
|
83
83
|
describe "when the client is requesting scopes it doesn't have yet" do
|
84
84
|
before { params['scope'] = 'wall_publish' }
|
85
85
|
it_should_behave_like "asks for user permission"
|
86
86
|
end
|
87
|
-
|
87
|
+
|
88
88
|
describe "and the authorization does not have a code" do
|
89
89
|
before { @authorization.update_attribute(:code, nil) }
|
90
|
-
|
90
|
+
|
91
91
|
it "generates a new code and redirects" do
|
92
92
|
Songkick::OAuth2::Model::Authorization.should_not_receive(:create)
|
93
93
|
Songkick::OAuth2::Model::Authorization.should_not_receive(:new)
|
@@ -97,13 +97,13 @@ describe Songkick::OAuth2::Provider do
|
|
97
97
|
response['location'].should == 'https://client.example.com/cb?code=new_code'
|
98
98
|
end
|
99
99
|
end
|
100
|
-
|
100
|
+
|
101
101
|
describe "and the authorization is expired" do
|
102
102
|
before { @authorization.update_attribute(:expires_at, 2.hours.ago) }
|
103
103
|
it_should_behave_like "asks for user permission"
|
104
104
|
end
|
105
105
|
end
|
106
|
-
|
106
|
+
|
107
107
|
describe "when there is already a completed authorization from the user" do
|
108
108
|
before do
|
109
109
|
@authorization = create_authorization(
|
@@ -112,39 +112,39 @@ describe Songkick::OAuth2::Provider do
|
|
112
112
|
:code => nil,
|
113
113
|
:access_token => Songkick::OAuth2.hashify('complete_token'))
|
114
114
|
end
|
115
|
-
|
115
|
+
|
116
116
|
it "immediately redirects with a new code" do
|
117
117
|
Songkick::OAuth2.should_receive(:random_string).and_return('new_code')
|
118
118
|
response = get(params)
|
119
119
|
response.code.to_i.should == 302
|
120
120
|
response['location'].should == 'https://client.example.com/cb?code=new_code'
|
121
121
|
end
|
122
|
-
|
122
|
+
|
123
123
|
describe "for token requests" do
|
124
124
|
before { params['response_type'] = 'token' }
|
125
|
-
|
125
|
+
|
126
126
|
it "immediately redirects with a new token" do
|
127
127
|
Songkick::OAuth2.should_receive(:random_string).and_return('new_access_token')
|
128
128
|
response = get(params)
|
129
129
|
response.code.to_i.should == 302
|
130
130
|
response['location'].should == 'https://client.example.com/cb#access_token=new_access_token'
|
131
131
|
end
|
132
|
-
|
132
|
+
|
133
133
|
describe "with an invalid client_id" do
|
134
134
|
before { params['client_id'] = 'unknown_id' }
|
135
|
-
|
135
|
+
|
136
136
|
it "does not generate any new tokens" do
|
137
137
|
Songkick::OAuth2.should_not_receive(:random_string)
|
138
138
|
get(params)
|
139
139
|
end
|
140
140
|
end
|
141
141
|
end
|
142
|
-
|
142
|
+
|
143
143
|
it "does not create a new Authorization" do
|
144
144
|
get(params)
|
145
145
|
Songkick::OAuth2::Model::Authorization.count.should == 1
|
146
146
|
end
|
147
|
-
|
147
|
+
|
148
148
|
it "keeps the code and access token on the Authorization" do
|
149
149
|
get(params)
|
150
150
|
authorization = Songkick::OAuth2::Model::Authorization.first
|
@@ -152,48 +152,48 @@ describe Songkick::OAuth2::Provider do
|
|
152
152
|
authorization.access_token_hash.should_not be_nil
|
153
153
|
end
|
154
154
|
end
|
155
|
-
|
155
|
+
|
156
156
|
describe "with no parameters" do
|
157
157
|
let(:params) { {} }
|
158
|
-
|
158
|
+
|
159
159
|
it "renders an error page" do
|
160
160
|
response = get(params)
|
161
161
|
validate_response(response, 400, 'WAT')
|
162
162
|
end
|
163
163
|
end
|
164
|
-
|
164
|
+
|
165
165
|
describe "with a redirect_uri and no client_id" do
|
166
166
|
let(:params) { {'redirect_uri' => 'http://evilsite.com/callback'} }
|
167
|
-
|
167
|
+
|
168
168
|
it "renders an error page" do
|
169
169
|
response = get(params)
|
170
170
|
validate_response(response, 400, 'WAT')
|
171
171
|
end
|
172
172
|
end
|
173
|
-
|
173
|
+
|
174
174
|
describe "with a client_id and a bad redirect_uri" do
|
175
175
|
let(:params) { {'redirect_uri' => 'http://evilsite.com/callback',
|
176
176
|
'client_id' => @client.client_id} }
|
177
|
-
|
177
|
+
|
178
178
|
it "redirects to the client's registered redirect_uri" do
|
179
179
|
response = get(params)
|
180
180
|
response.code.to_i.should == 302
|
181
181
|
response['location'].should == 'https://client.example.com/cb?error=invalid_request&error_description=Missing+required+parameter+response_type'
|
182
182
|
end
|
183
183
|
end
|
184
|
-
|
184
|
+
|
185
185
|
describe "with an invalid request" do
|
186
186
|
before { params.delete('response_type') }
|
187
|
-
|
187
|
+
|
188
188
|
it "redirects to the client's redirect_uri on error" do
|
189
189
|
response = get(params)
|
190
190
|
response.code.to_i.should == 302
|
191
191
|
response['location'].should == 'https://client.example.com/cb?error=invalid_request&error_description=Missing+required+parameter+response_type'
|
192
192
|
end
|
193
|
-
|
193
|
+
|
194
194
|
describe "with a state parameter" do
|
195
195
|
before { params['state'] = "Facebook\nmesses this\nup" }
|
196
|
-
|
196
|
+
|
197
197
|
it "redirects to the client, including the state param" do
|
198
198
|
response = get(params)
|
199
199
|
response.code.to_i.should == 302
|
@@ -202,55 +202,55 @@ describe Songkick::OAuth2::Provider do
|
|
202
202
|
end
|
203
203
|
end
|
204
204
|
end
|
205
|
-
|
205
|
+
|
206
206
|
describe "authorization confirmation from the user" do
|
207
207
|
let(:mock_auth) do
|
208
|
-
mock =
|
209
|
-
|
210
|
-
|
211
|
-
|
208
|
+
mock = double Songkick::OAuth2::Provider::Authorization,
|
209
|
+
:redirect_uri => 'http://example.com/',
|
210
|
+
:response_status => 302
|
211
|
+
|
212
212
|
Songkick::OAuth2::Provider::Authorization.stub(:new).and_return(mock)
|
213
213
|
mock
|
214
214
|
end
|
215
|
-
|
215
|
+
|
216
216
|
describe "without the user's permission" do
|
217
217
|
before { params['allow'] = '' }
|
218
|
-
|
218
|
+
|
219
219
|
it "does not grant access" do
|
220
220
|
mock_auth.should_receive(:deny_access!)
|
221
221
|
allow_or_deny(params)
|
222
222
|
end
|
223
|
-
|
223
|
+
|
224
224
|
it "redirects to the client with an error" do
|
225
225
|
response = allow_or_deny(params)
|
226
226
|
response.code.to_i.should == 302
|
227
227
|
response['location'].should == 'https://client.example.com/cb?error=access_denied&error_description=The+user+denied+you+access'
|
228
228
|
end
|
229
229
|
end
|
230
|
-
|
230
|
+
|
231
231
|
describe "with valid parameters and user permission" do
|
232
232
|
before { Songkick::OAuth2.stub(:random_string).and_return('foo') }
|
233
233
|
before { params['allow'] = '1' }
|
234
|
-
|
234
|
+
|
235
235
|
describe "for code requests" do
|
236
236
|
it "grants access" do
|
237
237
|
mock_auth.should_receive(:grant_access!)
|
238
238
|
allow_or_deny(params)
|
239
239
|
end
|
240
|
-
|
240
|
+
|
241
241
|
it "redirects to the client with an authorization code" do
|
242
242
|
response = allow_or_deny(params)
|
243
243
|
response.code.to_i.should == 302
|
244
244
|
response['location'].should == 'https://client.example.com/cb?code=foo'
|
245
245
|
end
|
246
|
-
|
246
|
+
|
247
247
|
it "passes the state parameter through" do
|
248
248
|
params['state'] = 'illinois'
|
249
249
|
response = allow_or_deny(params)
|
250
250
|
response.code.to_i.should == 302
|
251
251
|
response['location'].should == 'https://client.example.com/cb?code=foo&state=illinois'
|
252
252
|
end
|
253
|
-
|
253
|
+
|
254
254
|
it "passes the scope parameter through" do
|
255
255
|
params['scope'] = 'foo bar'
|
256
256
|
response = allow_or_deny(params)
|
@@ -258,28 +258,28 @@ describe Songkick::OAuth2::Provider do
|
|
258
258
|
response['location'].should == 'https://client.example.com/cb?code=foo&scope=foo+bar'
|
259
259
|
end
|
260
260
|
end
|
261
|
-
|
261
|
+
|
262
262
|
describe "for token requests" do
|
263
263
|
before { params['response_type'] = 'token' }
|
264
|
-
|
264
|
+
|
265
265
|
it "grants access" do
|
266
266
|
mock_auth.should_receive(:grant_access!)
|
267
267
|
allow_or_deny(params)
|
268
268
|
end
|
269
|
-
|
269
|
+
|
270
270
|
it "redirects to the client with an access token" do
|
271
271
|
response = allow_or_deny(params)
|
272
272
|
response.code.to_i.should == 302
|
273
273
|
response['location'].should == 'https://client.example.com/cb#access_token=foo&expires_in=10800'
|
274
274
|
end
|
275
|
-
|
275
|
+
|
276
276
|
it "passes the state parameter through" do
|
277
277
|
params['state'] = 'illinois'
|
278
278
|
response = allow_or_deny(params)
|
279
279
|
response.code.to_i.should == 302
|
280
280
|
response['location'].should == 'https://client.example.com/cb#access_token=foo&expires_in=10800&state=illinois'
|
281
281
|
end
|
282
|
-
|
282
|
+
|
283
283
|
it "passes the scope parameter through" do
|
284
284
|
params['scope'] = 'foo bar'
|
285
285
|
response = allow_or_deny(params)
|
@@ -287,28 +287,28 @@ describe Songkick::OAuth2::Provider do
|
|
287
287
|
response['location'].should == 'https://client.example.com/cb#access_token=foo&expires_in=10800&scope=foo+bar'
|
288
288
|
end
|
289
289
|
end
|
290
|
-
|
290
|
+
|
291
291
|
describe "for code_and_token requests" do
|
292
292
|
before { params['response_type'] = 'code_and_token' }
|
293
|
-
|
293
|
+
|
294
294
|
it "grants access" do
|
295
295
|
mock_auth.should_receive(:grant_access!)
|
296
296
|
allow_or_deny(params)
|
297
297
|
end
|
298
|
-
|
298
|
+
|
299
299
|
it "redirects to the client with an access token" do
|
300
300
|
response = allow_or_deny(params)
|
301
301
|
response.code.to_i.should == 302
|
302
302
|
response['location'].should == 'https://client.example.com/cb?code=foo#access_token=foo&expires_in=10800'
|
303
303
|
end
|
304
|
-
|
304
|
+
|
305
305
|
it "passes the state parameter through" do
|
306
306
|
params['state'] = 'illinois'
|
307
307
|
response = allow_or_deny(params)
|
308
308
|
response.code.to_i.should == 302
|
309
309
|
response['location'].should == 'https://client.example.com/cb?code=foo&state=illinois#access_token=foo&expires_in=10800'
|
310
310
|
end
|
311
|
-
|
311
|
+
|
312
312
|
it "passes the scope parameter through" do
|
313
313
|
params['scope'] = 'foo bar'
|
314
314
|
response = allow_or_deny(params)
|
@@ -318,7 +318,7 @@ describe Songkick::OAuth2::Provider do
|
|
318
318
|
end
|
319
319
|
end
|
320
320
|
end
|
321
|
-
|
321
|
+
|
322
322
|
describe "access token request" do
|
323
323
|
before do
|
324
324
|
@client = Factory(:client)
|
@@ -328,20 +328,20 @@ describe Songkick::OAuth2::Provider do
|
|
328
328
|
:code => 'a_fake_code',
|
329
329
|
:expires_at => 3.hours.from_now)
|
330
330
|
end
|
331
|
-
|
331
|
+
|
332
332
|
let(:auth_params) { { 'client_id' => @client.client_id,
|
333
333
|
'client_secret' => @client.client_secret }
|
334
334
|
}
|
335
|
-
|
335
|
+
|
336
336
|
describe "using authorization_code request" do
|
337
337
|
let(:query_params) { { 'client_id' => @client.client_id,
|
338
338
|
'grant_type' => 'authorization_code',
|
339
339
|
'code' => @authorization.code,
|
340
340
|
'redirect_uri' => @client.redirect_uri }
|
341
341
|
}
|
342
|
-
|
342
|
+
|
343
343
|
let(:params) { auth_params.merge(query_params) }
|
344
|
-
|
344
|
+
|
345
345
|
describe "with valid parameters" do
|
346
346
|
it "does not respond to GET" do
|
347
347
|
Songkick::OAuth2::Provider::Authorization.should_not_receive(:new)
|
@@ -352,7 +352,7 @@ describe Songkick::OAuth2::Provider do
|
|
352
352
|
'error_description' => 'Bad request: must be a POST request'
|
353
353
|
)
|
354
354
|
end
|
355
|
-
|
355
|
+
|
356
356
|
it "does not allow client credentials to be passed in the query string" do
|
357
357
|
Songkick::OAuth2::Provider::Authorization.should_not_receive(:new)
|
358
358
|
query_string = {'client_id' => params.delete('client_id'), 'client_secret' => params.delete('client_secret')}
|
@@ -362,10 +362,10 @@ describe Songkick::OAuth2::Provider do
|
|
362
362
|
'error_description' => 'Bad request: must not send client credentials in the URI'
|
363
363
|
)
|
364
364
|
end
|
365
|
-
|
365
|
+
|
366
366
|
describe "enforcing SSL" do
|
367
367
|
before { Songkick::OAuth2::Provider.enforce_ssl = true }
|
368
|
-
|
368
|
+
|
369
369
|
it "does not allow non-SSL requests" do
|
370
370
|
response = get(params)
|
371
371
|
validate_json_response(response, 400,
|
@@ -374,30 +374,30 @@ describe Songkick::OAuth2::Provider do
|
|
374
374
|
)
|
375
375
|
end
|
376
376
|
end
|
377
|
-
|
377
|
+
|
378
378
|
it "creates a Token when using Basic Auth" do
|
379
379
|
token = mock_request(Songkick::OAuth2::Provider::Exchange, :response_body => 'Hello')
|
380
380
|
Songkick::OAuth2::Provider::Exchange.should_receive(:new).with(@owner, params, nil).and_return(token)
|
381
381
|
post_basic_auth(auth_params, query_params)
|
382
382
|
end
|
383
|
-
|
383
|
+
|
384
384
|
it "creates a Token when passing params in the POST body" do
|
385
385
|
token = mock_request(Songkick::OAuth2::Provider::Exchange, :response_body => 'Hello')
|
386
386
|
Songkick::OAuth2::Provider::Exchange.should_receive(:new).with(@owner, params, nil).and_return(token)
|
387
387
|
post(params)
|
388
388
|
end
|
389
|
-
|
389
|
+
|
390
390
|
it "returns a successful response" do
|
391
391
|
Songkick::OAuth2.stub(:random_string).and_return('random_access_token')
|
392
392
|
response = post_basic_auth(auth_params, query_params)
|
393
393
|
validate_json_response(response, 200, 'access_token' => 'random_access_token', 'expires_in' => 10800)
|
394
394
|
end
|
395
|
-
|
395
|
+
|
396
396
|
describe "with a scope parameter" do
|
397
397
|
before do
|
398
398
|
@authorization.update_attribute(:scope, 'foo bar')
|
399
399
|
end
|
400
|
-
|
400
|
+
|
401
401
|
it "passes the scope back in the success response" do
|
402
402
|
Songkick::OAuth2.stub(:random_string).and_return('random_access_token')
|
403
403
|
response = post_basic_auth(auth_params, query_params)
|
@@ -409,10 +409,10 @@ describe Songkick::OAuth2::Provider do
|
|
409
409
|
end
|
410
410
|
end
|
411
411
|
end
|
412
|
-
|
412
|
+
|
413
413
|
describe "with invalid parameters" do
|
414
414
|
before { query_params.delete('code') }
|
415
|
-
|
415
|
+
|
416
416
|
it "returns an error response" do
|
417
417
|
response = post_basic_auth(auth_params, query_params)
|
418
418
|
validate_json_response(response, 400,
|
@@ -421,10 +421,10 @@ describe Songkick::OAuth2::Provider do
|
|
421
421
|
)
|
422
422
|
end
|
423
423
|
end
|
424
|
-
|
424
|
+
|
425
425
|
describe "with mismatched client_id in POST params and Basic Auth params" do
|
426
426
|
before { query_params['client_id'] = 'foo' }
|
427
|
-
|
427
|
+
|
428
428
|
it "returns an error response" do
|
429
429
|
response = post_basic_auth(auth_params, query_params)
|
430
430
|
validate_json_response(response, 400,
|
@@ -433,13 +433,13 @@ describe Songkick::OAuth2::Provider do
|
|
433
433
|
)
|
434
434
|
end
|
435
435
|
end
|
436
|
-
|
436
|
+
|
437
437
|
describe "when there is an Authorization with code and token" do
|
438
438
|
before do
|
439
439
|
@authorization.update_attributes(:code => 'pending_code', :access_token => 'working_token')
|
440
440
|
Songkick::OAuth2.stub(:random_string).and_return('random_access_token')
|
441
441
|
end
|
442
|
-
|
442
|
+
|
443
443
|
it "returns a new access token" do
|
444
444
|
response = post(params)
|
445
445
|
validate_json_response(response, 200,
|
@@ -447,7 +447,7 @@ describe Songkick::OAuth2::Provider do
|
|
447
447
|
'expires_in' => 10800
|
448
448
|
)
|
449
449
|
end
|
450
|
-
|
450
|
+
|
451
451
|
it "exchanges the code for the new token on the existing Authorization" do
|
452
452
|
post(params)
|
453
453
|
@authorization.reload
|
@@ -457,7 +457,7 @@ describe Songkick::OAuth2::Provider do
|
|
457
457
|
end
|
458
458
|
end
|
459
459
|
end
|
460
|
-
|
460
|
+
|
461
461
|
describe "protected resource request" do
|
462
462
|
before do
|
463
463
|
@authorization = create_authorization(
|
@@ -466,58 +466,58 @@ describe Songkick::OAuth2::Provider do
|
|
466
466
|
:access_token => 'magic-key',
|
467
467
|
:scope => 'profile')
|
468
468
|
end
|
469
|
-
|
469
|
+
|
470
470
|
shared_examples_for "protected resource" do
|
471
471
|
it "creates an AccessToken response" do
|
472
|
-
mock_token =
|
472
|
+
mock_token = double(Songkick::OAuth2::Provider::AccessToken)
|
473
473
|
mock_token.should_receive(:response_headers).and_return({})
|
474
474
|
mock_token.should_receive(:response_status).and_return(200)
|
475
475
|
mock_token.should_receive(:valid?).and_return(true)
|
476
476
|
Songkick::OAuth2::Provider::AccessToken.should_receive(:new).with(TestApp::User['Bob'], ['profile'], 'magic-key', nil).and_return(mock_token)
|
477
477
|
request('/user_profile', 'oauth_token' => 'magic-key')
|
478
478
|
end
|
479
|
-
|
479
|
+
|
480
480
|
it "allows access when the key is passed" do
|
481
481
|
response = request('/user_profile', 'oauth_token' => 'magic-key')
|
482
482
|
JSON.parse(response.body)['data'].should == 'Top secret'
|
483
483
|
response.code.to_i.should == 200
|
484
484
|
end
|
485
|
-
|
485
|
+
|
486
486
|
it "blocks access when the wrong key is passed" do
|
487
487
|
response = request('/user_profile', 'oauth_token' => 'is-the-password-books')
|
488
488
|
JSON.parse(response.body)['data'].should == 'No soup for you'
|
489
489
|
response.code.to_i.should == 401
|
490
490
|
response['WWW-Authenticate'].should == "OAuth realm='Demo App', error='invalid_token'"
|
491
491
|
end
|
492
|
-
|
492
|
+
|
493
493
|
it "blocks access when the no key is passed" do
|
494
494
|
response = request('/user_profile')
|
495
495
|
JSON.parse(response.body)['data'].should == 'No soup for you'
|
496
496
|
response.code.to_i.should == 401
|
497
497
|
response['WWW-Authenticate'].should == "OAuth realm='Demo App'"
|
498
498
|
end
|
499
|
-
|
499
|
+
|
500
500
|
describe "enforcing SSL" do
|
501
501
|
before { Songkick::OAuth2::Provider.enforce_ssl = true }
|
502
|
-
|
502
|
+
|
503
503
|
let(:authorization) do
|
504
504
|
Songkick::OAuth2::Model::Authorization.find_by_access_token_hash(Songkick::OAuth2.hashify('magic-key'))
|
505
505
|
end
|
506
|
-
|
506
|
+
|
507
507
|
it "blocks access when not using HTTPS" do
|
508
508
|
response = request('/user_profile', 'oauth_token' => 'magic-key')
|
509
509
|
JSON.parse(response.body)['data'].should == 'No soup for you'
|
510
510
|
response.code.to_i.should == 401
|
511
511
|
response['WWW-Authenticate'].should == "OAuth realm='Demo App', error='invalid_request'"
|
512
512
|
end
|
513
|
-
|
513
|
+
|
514
514
|
it "destroys the access token since it's been leaked" do
|
515
515
|
authorization.access_token_hash.should == Songkick::OAuth2.hashify('magic-key')
|
516
516
|
request('/user_profile', 'oauth_token' => 'magic-key')
|
517
517
|
authorization.reload
|
518
518
|
authorization.access_token_hash.should be_nil
|
519
519
|
end
|
520
|
-
|
520
|
+
|
521
521
|
it "keeps the access token if the wrong key is passed" do
|
522
522
|
authorization.access_token_hash.should == Songkick::OAuth2.hashify('magic-key')
|
523
523
|
request('/user_profile', 'oauth_token' => 'is-the-password-books')
|
@@ -526,34 +526,34 @@ describe Songkick::OAuth2::Provider do
|
|
526
526
|
end
|
527
527
|
end
|
528
528
|
end
|
529
|
-
|
529
|
+
|
530
530
|
describe "for header-based requests" do
|
531
531
|
def request(path, params = {})
|
532
532
|
access_token = params.delete('oauth_token')
|
533
|
-
http = Net::HTTP.new('localhost',
|
533
|
+
http = Net::HTTP.new('localhost', RequestHelpers::SERVER_PORT)
|
534
534
|
qs = params.map { |k,v| "#{ CGI.escape k.to_s }=#{ CGI.escape v.to_s }" }.join('&')
|
535
535
|
header = {'Authorization' => "OAuth #{access_token}"}
|
536
536
|
http.request_get(path + '?' + qs, header)
|
537
537
|
end
|
538
|
-
|
538
|
+
|
539
539
|
it_should_behave_like "protected resource"
|
540
540
|
end
|
541
|
-
|
541
|
+
|
542
542
|
describe "for GET requests" do
|
543
543
|
def request(path, params = {})
|
544
544
|
qs = params.map { |k,v| "#{ CGI.escape k.to_s }=#{ CGI.escape v.to_s }" }.join('&')
|
545
|
-
uri = URI.parse(
|
545
|
+
uri = URI.parse("http://localhost:#{RequestHelpers::SERVER_PORT}" + path + '?' + qs)
|
546
546
|
Net::HTTP.get_response(uri)
|
547
547
|
end
|
548
|
-
|
548
|
+
|
549
549
|
it_should_behave_like "protected resource"
|
550
550
|
end
|
551
|
-
|
551
|
+
|
552
552
|
describe "for POST requests" do
|
553
553
|
def request(path, params = {})
|
554
|
-
Net::HTTP.post_form(URI.parse(
|
554
|
+
Net::HTTP.post_form(URI.parse("http://localhost:#{RequestHelpers::SERVER_PORT}" + path), params)
|
555
555
|
end
|
556
|
-
|
556
|
+
|
557
557
|
it_should_behave_like "protected resource"
|
558
558
|
end
|
559
559
|
end
|