tpitale-rack-oauth2-server 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/CHANGELOG +202 -0
  2. data/Gemfile +16 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +604 -0
  5. data/Rakefile +90 -0
  6. data/VERSION +1 -0
  7. data/bin/oauth2-server +206 -0
  8. data/lib/rack-oauth2-server.rb +4 -0
  9. data/lib/rack/oauth2/admin/css/screen.css +347 -0
  10. data/lib/rack/oauth2/admin/images/loading.gif +0 -0
  11. data/lib/rack/oauth2/admin/images/oauth-2.png +0 -0
  12. data/lib/rack/oauth2/admin/js/application.coffee +220 -0
  13. data/lib/rack/oauth2/admin/js/jquery.js +166 -0
  14. data/lib/rack/oauth2/admin/js/jquery.tmpl.js +414 -0
  15. data/lib/rack/oauth2/admin/js/protovis-r3.2.js +277 -0
  16. data/lib/rack/oauth2/admin/js/sammy.js +5 -0
  17. data/lib/rack/oauth2/admin/js/sammy.json.js +5 -0
  18. data/lib/rack/oauth2/admin/js/sammy.oauth2.js +142 -0
  19. data/lib/rack/oauth2/admin/js/sammy.storage.js +5 -0
  20. data/lib/rack/oauth2/admin/js/sammy.title.js +5 -0
  21. data/lib/rack/oauth2/admin/js/sammy.tmpl.js +5 -0
  22. data/lib/rack/oauth2/admin/js/underscore.js +722 -0
  23. data/lib/rack/oauth2/admin/views/client.tmpl +58 -0
  24. data/lib/rack/oauth2/admin/views/clients.tmpl +52 -0
  25. data/lib/rack/oauth2/admin/views/edit.tmpl +80 -0
  26. data/lib/rack/oauth2/admin/views/index.html +39 -0
  27. data/lib/rack/oauth2/admin/views/no_access.tmpl +4 -0
  28. data/lib/rack/oauth2/models.rb +27 -0
  29. data/lib/rack/oauth2/models/access_grant.rb +54 -0
  30. data/lib/rack/oauth2/models/access_token.rb +129 -0
  31. data/lib/rack/oauth2/models/auth_request.rb +61 -0
  32. data/lib/rack/oauth2/models/client.rb +93 -0
  33. data/lib/rack/oauth2/rails.rb +105 -0
  34. data/lib/rack/oauth2/server.rb +458 -0
  35. data/lib/rack/oauth2/server/admin.rb +250 -0
  36. data/lib/rack/oauth2/server/errors.rb +104 -0
  37. data/lib/rack/oauth2/server/helper.rb +147 -0
  38. data/lib/rack/oauth2/server/practice.rb +79 -0
  39. data/lib/rack/oauth2/server/railtie.rb +24 -0
  40. data/lib/rack/oauth2/server/utils.rb +30 -0
  41. data/lib/rack/oauth2/sinatra.rb +71 -0
  42. data/rack-oauth2-server.gemspec +24 -0
  43. data/rails/init.rb +11 -0
  44. data/test/admin/api_test.rb +228 -0
  45. data/test/admin/ui_test.rb +38 -0
  46. data/test/oauth/access_grant_test.rb +276 -0
  47. data/test/oauth/access_token_test.rb +311 -0
  48. data/test/oauth/authorization_test.rb +298 -0
  49. data/test/oauth/server_methods_test.rb +292 -0
  50. data/test/rails2/app/controllers/api_controller.rb +40 -0
  51. data/test/rails2/app/controllers/application_controller.rb +2 -0
  52. data/test/rails2/app/controllers/oauth_controller.rb +17 -0
  53. data/test/rails2/config/environment.rb +19 -0
  54. data/test/rails2/config/environments/test.rb +0 -0
  55. data/test/rails2/config/routes.rb +13 -0
  56. data/test/rails3/app/controllers/api_controller.rb +40 -0
  57. data/test/rails3/app/controllers/application_controller.rb +2 -0
  58. data/test/rails3/app/controllers/oauth_controller.rb +17 -0
  59. data/test/rails3/config/application.rb +19 -0
  60. data/test/rails3/config/environment.rb +2 -0
  61. data/test/rails3/config/routes.rb +12 -0
  62. data/test/setup.rb +120 -0
  63. data/test/sinatra/my_app.rb +69 -0
  64. metadata +145 -0
@@ -0,0 +1,38 @@
1
+ require "test/setup"
2
+
3
+ class AdminUiTest < Test::Unit::TestCase
4
+ context "/" do
5
+ setup { get "/oauth/admin" }
6
+ should "return OK" do
7
+ assert_equal 200, last_response.status
8
+ end
9
+ should "return HTML page" do
10
+ assert_match "<html>", last_response.body
11
+ end
12
+ end
13
+
14
+ context "force SSL" do
15
+ setup { Server::Admin.force_ssl = true }
16
+
17
+ context "HTTP request" do
18
+ setup { get "/oauth/admin" }
19
+
20
+ should "redirect to HTTPS" do
21
+ assert_equal 302, last_response.status
22
+ assert_equal "https://example.org/oauth/admin", last_response.location
23
+ end
24
+ end
25
+
26
+ context "HTTPS request" do
27
+ setup { get "https://example.org/oauth/admin" }
28
+
29
+ should "serve request" do
30
+ assert_equal 200, last_response.status
31
+ assert_match "<html>", last_response.body
32
+ end
33
+ end
34
+
35
+ teardown { Server::Admin.force_ssl = false }
36
+ end
37
+
38
+ end
@@ -0,0 +1,276 @@
1
+ require "test/setup"
2
+
3
+
4
+ # 4. Obtaining an Access Token
5
+ class AccessGrantTest < Test::Unit::TestCase
6
+ module Helpers
7
+
8
+ def should_return_error(error)
9
+ should "respond with status 400 (Bad Request)" do
10
+ assert_equal 400, last_response.status
11
+ end
12
+ should "respond with JSON document" do
13
+ assert_equal "application/json", last_response.content_type
14
+ end
15
+ should "respond with error code #{error}" do
16
+ assert_equal error.to_s, JSON.parse(last_response.body)["error"]
17
+ end
18
+ end
19
+
20
+ def should_respond_with_authentication_error(error)
21
+ should "respond with status 401 (Unauthorized)" do
22
+ assert_equal 401, last_response.status
23
+ end
24
+ should "respond with authentication method OAuth" do
25
+ assert_equal "OAuth", last_response["WWW-Authenticate"].split.first
26
+ end
27
+ should "respond with realm" do
28
+ assert_match " realm=\"example.org\"", last_response["WWW-Authenticate"]
29
+ end
30
+ should "respond with error code #{error}" do
31
+ assert_match " error=\"#{error}\"", last_response["WWW-Authenticate"]
32
+ end
33
+ end
34
+
35
+ def should_respond_with_access_token(scope = "read write")
36
+ should "respond with status 200" do
37
+ assert_equal 200, last_response.status
38
+ end
39
+ should "respond with JSON document" do
40
+ assert_equal "application/json", last_response.content_type
41
+ end
42
+ should "respond with cache control no-store" do
43
+ assert_equal "no-store", last_response["Cache-Control"]
44
+ end
45
+ should "not respond with error code" do
46
+ assert JSON.parse(last_response.body)["error"].nil?
47
+ end
48
+ should "response with access token" do
49
+ assert_match /[a-f0-9]{32}/i, JSON.parse(last_response.body)["access_token"]
50
+ end
51
+ should "response with scope" do
52
+ assert_equal scope || "", JSON.parse(last_response.body)["scope"]
53
+ end
54
+ end
55
+
56
+
57
+ end
58
+ extend Helpers
59
+
60
+ def setup
61
+ super
62
+ # Get authorization code.
63
+ params = { :redirect_uri=>client.redirect_uri, :client_id=>client.id, :client_secret=>client.secret, :response_type=>"code",
64
+ :scope=>"read write", :state=>"bring this back" }
65
+ get "/oauth/authorize?" + Rack::Utils.build_query(params)
66
+ get last_response["Location"] if last_response.status == 303
67
+ authorization = last_response.body[/authorization:\s*(\S+)/, 1]
68
+ post "/oauth/grant", :authorization=>authorization
69
+ @code = Rack::Utils.parse_query(URI.parse(last_response["Location"]).query)["code"]
70
+ end
71
+
72
+ def request_none(scope = nil)
73
+ basic_authorize client.id, client.secret
74
+ # Note: This grant_type becomes "client_credentials" in version 11 of the OAuth 2.0 spec
75
+ params = { :grant_type=>"none", :scope=>"read write" }
76
+ params[:scope] = scope if scope
77
+ post "/oauth/access_token", params
78
+ end
79
+
80
+ def request_access_token(changes = nil)
81
+ params = { :client_id=>client.id, :client_secret=>client.secret, :scope=>"read write",
82
+ :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri }.merge(changes || {})
83
+ basic_authorize params.delete(:client_id), params.delete(:client_secret)
84
+ post "/oauth/access_token", params
85
+ end
86
+
87
+ def request_with_username_password(username, password, scope = nil)
88
+ basic_authorize client.id, client.secret
89
+ params = { :grant_type=>"password" }
90
+ params[:scope] = scope if scope
91
+ params[:username] = username if username
92
+ params[:password] = password if password
93
+ post "/oauth/access_token", params
94
+ end
95
+
96
+
97
+ # 4. Obtaining an Access Token
98
+
99
+ context "GET request" do
100
+ setup { get "/oauth/access_token" }
101
+
102
+ should "respond with status 405 (Method Not Allowed)" do
103
+ assert_equal 405, last_response.status
104
+ end
105
+ end
106
+
107
+ context "no client ID" do
108
+ setup { request_access_token :client_id=>nil }
109
+ should_respond_with_authentication_error :invalid_client
110
+ end
111
+
112
+ context "invalid client ID" do
113
+ setup { request_access_token :client_id=>"foobar" }
114
+ should_respond_with_authentication_error :invalid_client
115
+ end
116
+
117
+ context "client ID but no such client" do
118
+ setup { request_access_token :client_id=>"4cc7bc483321e814b8000000" }
119
+ should_respond_with_authentication_error :invalid_client
120
+ end
121
+
122
+ context "no client secret" do
123
+ setup { request_access_token :client_secret=>nil }
124
+ should_respond_with_authentication_error :invalid_client
125
+ end
126
+
127
+ context "wrong client secret" do
128
+ setup { request_access_token :client_secret=>"plain wrong" }
129
+ should_respond_with_authentication_error :invalid_client
130
+ end
131
+
132
+ context "client revoked" do
133
+ setup do
134
+ client.revoke!
135
+ request_access_token
136
+ end
137
+ should_respond_with_authentication_error :invalid_client
138
+ end
139
+
140
+ context "unsupported grant type" do
141
+ setup { request_access_token :grant_type=>"bogus" }
142
+ should_return_error :unsupported_grant_type
143
+ end
144
+
145
+ # 4.1.1. Authorization Code
146
+
147
+ context "no authorization code" do
148
+ setup { request_access_token :code=>nil }
149
+ should_return_error :invalid_grant
150
+ end
151
+
152
+ context "unknown authorization code" do
153
+ setup { request_access_token :code=>"unknown" }
154
+ should_return_error :invalid_grant
155
+ end
156
+
157
+ context "authorization code for different client" do
158
+ setup do
159
+ grant = Server::AccessGrant.create("foo bar", Server.register(:scope=>%w{read write}), "read write", nil)
160
+ request_access_token :code=>grant.code
161
+ end
162
+ should_return_error :invalid_grant
163
+ end
164
+
165
+ context "authorization code revoked" do
166
+ setup do
167
+ Server::AccessGrant.from_code(@code).revoke!
168
+ request_access_token
169
+ end
170
+ should_return_error :invalid_grant
171
+ end
172
+
173
+ context "mistmatched redirect URI" do
174
+ setup { request_access_token :redirect_uri=>"http://uberclient.dot/oz" }
175
+ should_return_error :invalid_grant
176
+ end
177
+
178
+ context "no redirect URI to match" do
179
+ setup do
180
+ @client = Server.register(:display_name=>"No rediret", :scope=>"read write")
181
+ grant = Server::AccessGrant.create("foo bar", client, "read write", nil)
182
+ request_access_token :code=>grant.code, :redirect_uri=>"http://uberclient.dot/oz"
183
+ end
184
+ should_respond_with_access_token
185
+ end
186
+
187
+ context "access grant expired" do
188
+ setup do
189
+ Timecop.travel 300 do
190
+ request_access_token
191
+ end
192
+ end
193
+ should_return_error :invalid_grant
194
+ end
195
+
196
+ context "access grant spent" do
197
+ setup do
198
+ request_access_token
199
+ request_access_token
200
+ end
201
+ should_return_error :invalid_grant
202
+ end
203
+
204
+ # 4.1.2. Resource Owner Password Credentials
205
+
206
+ context "no username" do
207
+ setup { request_with_username_password nil, "more" }
208
+ should_return_error :invalid_grant
209
+ end
210
+
211
+ context "no password" do
212
+ setup { request_with_username_password nil, "more" }
213
+ should_return_error :invalid_grant
214
+ end
215
+
216
+ context "not authorized" do
217
+ setup { request_with_username_password "cowbell", "less" }
218
+ should_return_error :invalid_grant
219
+ end
220
+
221
+ context "no scope specified" do
222
+ setup { request_with_username_password "cowbell", "more" }
223
+ should_respond_with_access_token "oauth-admin read write"
224
+ end
225
+
226
+ context "given scope" do
227
+ setup { request_with_username_password "cowbell", "more", "read" }
228
+ should_respond_with_access_token "read"
229
+ end
230
+
231
+ context "unsupported scope" do
232
+ setup { request_with_username_password "cowbell", "more", "read write math" }
233
+ should_return_error :invalid_scope
234
+ end
235
+
236
+ context "authenticator with 4 parameters" do
237
+ setup do
238
+ @old = config.authenticator
239
+ config.authenticator = lambda do |username, password, client_id, scope|
240
+ @client_id = client_id
241
+ @scope = scope
242
+ "Batman"
243
+ end
244
+ request_with_username_password "cowbell", "more", "read"
245
+ end
246
+
247
+ should_respond_with_access_token "read"
248
+ should "receive client identifier" do
249
+ assert_equal client.id, @client_id
250
+ end
251
+ should "receive scope" do
252
+ assert_equal %w{read}, @scope
253
+ end
254
+
255
+ teardown { config.authenticator = @old }
256
+ end
257
+
258
+
259
+ # 4.2. Access Token Response
260
+
261
+ context "using none" do
262
+ setup { request_none }
263
+ should_respond_with_access_token "read write"
264
+ end
265
+
266
+ context "using authorization code" do
267
+ setup { request_access_token }
268
+ should_respond_with_access_token "read write"
269
+ end
270
+
271
+ context "using username/password" do
272
+ setup { request_with_username_password "cowbell", "more", "read" }
273
+ should_respond_with_access_token "read"
274
+ end
275
+
276
+ end
@@ -0,0 +1,311 @@
1
+ require "test/setup"
2
+
3
+
4
+ # 5. Accessing a Protected Resource
5
+ class AccessTokenTest < Test::Unit::TestCase
6
+ module Helpers
7
+
8
+ def should_return_resource(content)
9
+ should "respond with status 200" do
10
+ assert_equal 200, last_response.status
11
+ end
12
+ should "respond with resource name" do
13
+ assert_equal content, last_response.body
14
+ end
15
+ end
16
+
17
+ def should_fail_authentication(error = nil)
18
+ should "respond with status 401 (Unauthorized)" do
19
+ assert_equal 401, last_response.status
20
+ end
21
+ should "respond with authentication method OAuth" do
22
+ assert_equal "OAuth", last_response["WWW-Authenticate"].split.first
23
+ end
24
+ should "respond with realm" do
25
+ assert_match " realm=\"example.org\"", last_response["WWW-Authenticate"]
26
+ end
27
+ if error
28
+ should "respond with error code #{error}" do
29
+ assert_match " error=\"#{error}\"", last_response["WWW-Authenticate"]
30
+ end
31
+ else
32
+ should "not respond with error code" do
33
+ assert !last_response["WWW-Authenticate"]["error="]
34
+ end
35
+ end
36
+ end
37
+
38
+ end
39
+ extend Helpers
40
+
41
+
42
+ def setup
43
+ super
44
+ # Get authorization code.
45
+ params = { :redirect_uri=>client.redirect_uri, :client_id=>client.id, :client_secret=>client.secret, :response_type=>"code",
46
+ :scope=>"read write", :state=>"bring this back" }
47
+ get "/oauth/authorize?" + Rack::Utils.build_query(params)
48
+ get last_response["Location"] if last_response.status == 303
49
+ authorization = last_response.body[/authorization:\s*(\S+)/, 1]
50
+ post "/oauth/grant", :authorization=>authorization
51
+ code = Rack::Utils.parse_query(URI.parse(last_response["Location"]).query)["code"]
52
+ # Get access token
53
+ basic_authorize client.id, client.secret
54
+ post "/oauth/access_token", :scope=>"read write", :grant_type=>"authorization_code", :code=>code, :redirect_uri=>client.redirect_uri
55
+ @token = JSON.parse(last_response.body)["access_token"]
56
+ header "Authorization", nil
57
+ end
58
+
59
+ def with_token(token = @token)
60
+ header "Authorization", "OAuth #{token}"
61
+ end
62
+
63
+
64
+ # 5. Accessing a Protected Resource
65
+
66
+ context "public resource" do
67
+ context "no authorization" do
68
+ setup { get "/public" }
69
+ should_return_resource "HAI"
70
+ end
71
+
72
+ context "with authorization" do
73
+ setup do
74
+ with_token
75
+ get "/public"
76
+ end
77
+ should_return_resource "HAI from Batman"
78
+ end
79
+ end
80
+
81
+ context "private resource" do
82
+ context "no authorization" do
83
+ setup { get "/private" }
84
+ should_fail_authentication
85
+ end
86
+
87
+ context "HTTP authentication" do
88
+ context "valid token" do
89
+ setup do
90
+ with_token
91
+ get "/private"
92
+ end
93
+ should_return_resource "Shhhh"
94
+ end
95
+
96
+ context "unknown token" do
97
+ setup do
98
+ with_token "dingdong"
99
+ get "/private"
100
+ end
101
+ should_fail_authentication :invalid_token
102
+ end
103
+
104
+ context "revoked HTTP token" do
105
+ setup do
106
+ Server::AccessToken.from_token(@token).revoke!
107
+ with_token
108
+ get "/private"
109
+ end
110
+ should_fail_authentication :invalid_token
111
+ end
112
+
113
+ context "revoked client" do
114
+ setup do
115
+ client.revoke!
116
+ with_token
117
+ get "/private"
118
+ end
119
+ should_fail_authentication :invalid_token
120
+ end
121
+ end
122
+
123
+ # 5.1.2. URI Query Parameter
124
+
125
+ context "query parameter" do
126
+ context "default mode" do
127
+ setup { get "/private?oauth_token=#{@token}" }
128
+ should_fail_authentication
129
+ end
130
+
131
+ context "enabled" do
132
+ setup do
133
+ config.param_authentication = true
134
+ end
135
+
136
+ context "valid token" do
137
+ setup { get "/private?oauth_token=#{@token}" }
138
+ should_return_resource "Shhhh"
139
+ end
140
+
141
+ context "invalid token" do
142
+ setup { get "/private?oauth_token=dingdong" }
143
+ should_fail_authentication :invalid_token
144
+ end
145
+
146
+ teardown do
147
+ config.param_authentication = false
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ context "POST" do
154
+ context "no authorization" do
155
+ setup { post "/change" }
156
+ should_fail_authentication
157
+ end
158
+
159
+ context "HTTP authentication" do
160
+ context "valid token" do
161
+ setup do
162
+ with_token
163
+ post "/change"
164
+ end
165
+ should_return_resource "Woot!"
166
+ end
167
+
168
+ context "unknown token" do
169
+ setup do
170
+ with_token "dingdong"
171
+ post "/change"
172
+ end
173
+ should_fail_authentication :invalid_token
174
+ end
175
+
176
+ end
177
+
178
+ # 5.1.3. Form-Encoded Body Parameter
179
+
180
+ context "body parameter" do
181
+ context "default mode" do
182
+ setup { post "/change", :oauth_token=>@token }
183
+ should_fail_authentication
184
+ end
185
+
186
+ context "enabled" do
187
+ setup do
188
+ config.param_authentication = true
189
+ end
190
+
191
+ context "valid token" do
192
+ setup { post "/change", :oauth_token=>@token }
193
+ should_return_resource "Woot!"
194
+ end
195
+
196
+ context "invalid token" do
197
+ setup { post "/change", :oauth_token=>"dingdong" }
198
+ should_fail_authentication :invalid_token
199
+ end
200
+
201
+ teardown do
202
+ config.param_authentication = false
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+
209
+ context "insufficient scope" do
210
+ context "valid token" do
211
+ setup do
212
+ with_token
213
+ get "/calc"
214
+ end
215
+
216
+ should "respond with status 403 (Forbidden)" do
217
+ assert_equal 403, last_response.status
218
+ end
219
+ should "respond with authentication method OAuth" do
220
+ assert_equal "OAuth", last_response["WWW-Authenticate"].split.first
221
+ end
222
+ should "respond with realm" do
223
+ assert_match " realm=\"example.org\"", last_response["WWW-Authenticate"]
224
+ end
225
+ should "respond with error code insufficient_scope" do
226
+ assert_match " error=\"insufficient_scope\"", last_response["WWW-Authenticate"]
227
+ end
228
+ should "respond with scope name" do
229
+ assert_match " scope=\"math\"", last_response["WWW-Authenticate"]
230
+ end
231
+ end
232
+ end
233
+
234
+
235
+ context "setting resource" do
236
+ context "authenticated" do
237
+ setup do
238
+ with_token
239
+ get "/user"
240
+ end
241
+
242
+ should "render user name" do
243
+ assert_equal "Batman", last_response.body
244
+ end
245
+ end
246
+
247
+ context "not authenticated" do
248
+ setup do
249
+ get "/user"
250
+ end
251
+
252
+ should "not render user name" do
253
+ assert last_response.body.empty?
254
+ end
255
+ end
256
+ end
257
+
258
+ context "list tokens" do
259
+ setup do
260
+ @other = Server.token_for("foobar", client.id, "read")
261
+ get "/list_tokens"
262
+ end
263
+
264
+ should "return access token" do
265
+ assert_contains last_response.body.split, @token
266
+ end
267
+
268
+ should "not return other resource's token" do
269
+ assert !last_response.body.split.include?(@other)
270
+ end
271
+ end
272
+
273
+
274
+ context "with specific host" do
275
+ context "right host" do
276
+ setup do
277
+ get "http://example.org/public"
278
+ end
279
+ # Right host, but not authenticated
280
+ should_return_resource "HAI"
281
+ end
282
+
283
+ context "wrong host" do
284
+ setup do
285
+ with_token
286
+ get "http://wrong.org/public"
287
+ end
288
+ # Wrong host, not checking credentials
289
+ should_return_resource "HAI"
290
+ end
291
+ end
292
+
293
+
294
+ context "with specific path" do
295
+ setup { config.path = "/private" }
296
+
297
+ context "outside path" do
298
+ setup { with_token ; get "http://example.org/public" }
299
+ # Not authenticated
300
+ should_return_resource "HAI"
301
+ end
302
+
303
+ context "inside path" do
304
+ setup { with_token ; get "http://example.org/private" }
305
+ # Authenticated
306
+ should_return_resource "Shhhh"
307
+ end
308
+
309
+ teardown { config.path = nil }
310
+ end
311
+ end