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