tpitale-rack-oauth2-server 2.2.1

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.
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