rack-oauth2-server 1.0.beta

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.
@@ -0,0 +1,24 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ module Utils
6
+ module_function
7
+
8
+ # Parses the redirect URL, normalizes it and returns a URI object.
9
+ #
10
+ # Raises InvalidRequestError if not an absolute HTTP/S URL.
11
+ def parse_redirect_uri(redirect_uri)
12
+ uri = URI.parse(redirect_uri).normalize
13
+ raise InvalidRequestError, "Redirect URL must be absolute URL" unless uri.absolute? && uri.host
14
+ raise InvalidRequestError, "Redirect URL must point to HTTP/S location" unless uri.scheme == "http" || uri.scheme == "https"
15
+ uri
16
+ rescue
17
+ raise InvalidRequestError, "Redirect URL looks fishy to me"
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ VERSION = "1.0.beta"
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,71 @@
1
+ require "rack/oauth2/server"
2
+
3
+ module Rack
4
+ module OAuth2
5
+
6
+ # Sinatra support.
7
+ #
8
+ # Adds oauth instance method that returns Rack::OAuth2::Helper, see there for
9
+ # more details.
10
+ #
11
+ # Adds oauth_required class method. Use this filter with paths that require
12
+ # authentication, and with paths that require client to have a specific
13
+ # access scope.
14
+ #
15
+ # Adds oauth setting you can use to configure the module (e.g. setting
16
+ # available scopes, see example).
17
+ #
18
+ # @example
19
+ # require "rack/oauth2/sinatra"
20
+ # class MyApp < Sinatra::Base
21
+ # register Rack::OAuth2::Sinatra
22
+ # oauth[:scopes] = %w{read write}
23
+ #
24
+ # oauth_required "/api"
25
+ # oauth_required "/api/edit", :scope=>"write"
26
+ #
27
+ # before { @user = User.find(oauth.resource) if oauth.authenticated? }
28
+ # end
29
+ #
30
+ # @see Helpers
31
+ module Sinatra
32
+
33
+ # Adds before filter to require authentication on all the listed paths.
34
+ # Use the :scope option if client must also have access to that scope.
35
+ #
36
+ # @param [String, ...] path One or more paths that require authentication
37
+ # @param [optional, Hash] options Currently only :scope is supported.
38
+ def oauth_required(*args)
39
+ options = args.pop if Hash === args.last
40
+ scope = options[:scope] if options
41
+ args.each do |path|
42
+ before path do
43
+ if oauth.authenticated?
44
+ if scope && !oauth.scope.include?(scope)
45
+ oauth.no_scope! scope
46
+ end
47
+ else
48
+ oauth.no_access!
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ module Helpers
55
+ # Returns the OAuth helper.
56
+ #
57
+ # @return [Server::Helper]
58
+ def oauth
59
+ @oauth ||= Rack::OAuth2::Server::Helper.new(request, response)
60
+ end
61
+ end
62
+
63
+ def self.registered(base)
64
+ base.helpers Helpers
65
+ base.set :oauth, {}
66
+ base.use Rack::OAuth2::Server, base.settings.oauth
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,25 @@
1
+ $: << File.dirname(__FILE__) + "/lib"
2
+ require "rack/oauth2/server/version"
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "rack-oauth2-server"
6
+ spec.version = Rack::OAuth2::Server::VERSION
7
+ spec.author = "Assaf Arkin"
8
+ spec.email = "assaf@labnotes.org"
9
+ spec.homepage = "http://github.com/assaf/#{spec.name}"
10
+ spec.summary = "OAuth 2.0 Authorization Server as a Rack module"
11
+ spec.description = "Because you don't allow strangers into your app, and OAuth 2.0 is the new awesome."
12
+ spec.post_install_message = ""
13
+
14
+ spec.files = Dir["{bin,lib,test}/**/*", "CHANGELOG", "MIT-LICENSE", "README.rdoc", "Rakefile", "Gemfile", "*.gemspec"]
15
+
16
+ spec.has_rdoc = true
17
+ spec.extra_rdoc_files = "README.rdoc", "CHANGELOG"
18
+ spec.rdoc_options = "--title", "rack-oauth2-server #{spec.version}", "--main", "README.rdoc",
19
+ "--webcvs", "http://github.com/assaf/#{spec.name}"
20
+
21
+ spec.required_ruby_version = '>= 1.8.7'
22
+ spec.add_dependency "rack", "~>1"
23
+ spec.add_dependency "mongo", "~>1"
24
+ spec.add_dependency "bson_ext"
25
+ end
@@ -0,0 +1,216 @@
1
+ require File.dirname(__FILE__) + "/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
+ post "/oauth/grant"
67
+ @code = Rack::Utils.parse_query(URI.parse(last_response["Location"]).query)["code"]
68
+ end
69
+
70
+ def request_access_token(changes = nil)
71
+ params = { :client_id=>client.id, :client_secret=>client.secret, :scope=>"read write",
72
+ :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri }.merge(changes || {})
73
+ basic_authorize params.delete(:client_id), params.delete(:client_secret)
74
+ post "/oauth/access_token", params
75
+ end
76
+
77
+ def request_with_username_password(username, password, scope = "read write")
78
+ basic_authorize client.id, client.secret
79
+ params = { :grant_type=>"password", :scope=>scope }
80
+ params[:username] = username if username
81
+ params[:password] = password if password
82
+ post "/oauth/access_token", params
83
+ end
84
+
85
+
86
+ # 4. Obtaining an Access Token
87
+
88
+ context "GET request" do
89
+ setup { get "/oauth/access_token" }
90
+
91
+ should "respond with status 405 (Method Not Allowed)" do
92
+ assert_equal 405, last_response.status
93
+ end
94
+ end
95
+
96
+ context "no client ID" do
97
+ setup { request_access_token :client_id=>nil }
98
+ should_respond_with_authentication_error :invalid_client
99
+ end
100
+
101
+ context "invalid client ID" do
102
+ setup { request_access_token :client_id=>"foobar" }
103
+ should_respond_with_authentication_error :invalid_client
104
+ end
105
+
106
+ context "client ID but no such client" do
107
+ setup { request_access_token :client_id=>"4cc7bc483321e814b8000000" }
108
+ should_respond_with_authentication_error :invalid_client
109
+ end
110
+
111
+ context "no client secret" do
112
+ setup { request_access_token :client_secret=>nil }
113
+ should_respond_with_authentication_error :invalid_client
114
+ end
115
+
116
+ context "wrong client secret" do
117
+ setup { request_access_token :client_secret=>"plain wrong" }
118
+ should_respond_with_authentication_error :invalid_client
119
+ end
120
+
121
+ context "client revoked" do
122
+ setup do
123
+ client.revoke!
124
+ request_access_token
125
+ end
126
+ should_respond_with_authentication_error :invalid_client
127
+ end
128
+
129
+ context "unsupported grant type" do
130
+ setup { request_access_token :grant_type=>"bogus" }
131
+ should_return_error :unsupported_grant_type
132
+ end
133
+
134
+
135
+ # 4.1.1. Authorization Code
136
+
137
+ context "no authorization code" do
138
+ setup { request_access_token :code=>nil }
139
+ should_return_error :invalid_grant
140
+ end
141
+
142
+ context "unknown authorization code" do
143
+ setup { request_access_token :code=>"unknown" }
144
+ should_return_error :invalid_grant
145
+ end
146
+
147
+ context "authorization code for different client" do
148
+ setup do
149
+ grant = Rack::OAuth2::Server::AccessGrant.create("foo bar", "read write", "4cc7bc483321e814b8000000", nil)
150
+ request_access_token :code=>grant.code
151
+ end
152
+ should_return_error :invalid_grant
153
+ end
154
+
155
+ context "authorization code revoked" do
156
+ setup do
157
+ Rack::OAuth2::Server::AccessGrant.from_code(@code).revoke!
158
+ request_access_token
159
+ end
160
+ should_return_error :invalid_grant
161
+ end
162
+
163
+ context "mistmatched redirect URI" do
164
+ setup { request_access_token :redirect_uri=>"http://uberclient.dot/oz" }
165
+ should_return_error :invalid_grant
166
+ end
167
+
168
+ context "no redirect URI to match" do
169
+ setup do
170
+ grant = Rack::OAuth2::Server::AccessGrant.create("foo bar", "read write", client.id, nil)
171
+ request_access_token :code=>grant.code, :redirect_uri=>"http://uberclient.dot/oz"
172
+ end
173
+ should_respond_with_access_token
174
+ end
175
+
176
+
177
+ # 4.1.2. Resource Owner Password Credentials
178
+
179
+ context "no username" do
180
+ setup { request_with_username_password nil, "more" }
181
+ should_return_error :invalid_grant
182
+ end
183
+
184
+ context "no password" do
185
+ setup { request_with_username_password nil, "more" }
186
+ should_return_error :invalid_grant
187
+ end
188
+
189
+ context "not authorized" do
190
+ setup { request_with_username_password "cowbell", "less" }
191
+ should_return_error :invalid_grant
192
+ end
193
+
194
+ context "no scope specified" do
195
+ setup { request_with_username_password "cowbell", "more", nil }
196
+ should_respond_with_access_token nil
197
+ end
198
+
199
+ context "unsupported scope" do
200
+ setup { request_with_username_password "cowbell", "more", "read write math" }
201
+ should_return_error :invalid_scope
202
+ end
203
+
204
+ # 4.2. Access Token Response
205
+
206
+ context "using authorization code" do
207
+ setup { request_access_token }
208
+ should_respond_with_access_token "read write"
209
+ end
210
+
211
+ context "using username/password" do
212
+ setup { request_with_username_password "cowbell", "more", "read" }
213
+ should_respond_with_access_token "read"
214
+ end
215
+
216
+ end
@@ -0,0 +1,237 @@
1
+ require File.dirname(__FILE__) + "/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
+ post "/oauth/grant"
49
+ code = Rack::Utils.parse_query(URI.parse(last_response["Location"]).query)["code"]
50
+ # Get access token
51
+ basic_authorize client.id, client.secret
52
+ post "/oauth/access_token", :scope=>"read write", :grant_type=>"authorization_code", :code=>code, :redirect_uri=>client.redirect_uri
53
+ @token = JSON.parse(last_response.body)["access_token"]
54
+ header "Authorization", nil
55
+ end
56
+
57
+ def with_token(token = @token)
58
+ header "Authorization", "OAuth #{token}"
59
+ end
60
+
61
+
62
+ # 5. Accessing a Protected Resource
63
+
64
+ context "public resource" do
65
+ context "no authorization" do
66
+ setup { get "/public" }
67
+ should_return_resource "HAI"
68
+ end
69
+
70
+ context "with authorization" do
71
+ setup do
72
+ with_token
73
+ get "/public"
74
+ end
75
+ should_return_resource "HAI from Superman"
76
+ end
77
+ end
78
+
79
+ context "private resource" do
80
+ context "no authorization" do
81
+ setup { get "/private" }
82
+ should_fail_authentication
83
+ end
84
+
85
+ context "HTTP authentication" do
86
+ context "valid token" do
87
+ setup do
88
+ with_token
89
+ get "/private"
90
+ end
91
+ should_return_resource "Shhhh"
92
+ end
93
+
94
+ context "unknown token" do
95
+ setup do
96
+ with_token "dingdong"
97
+ get "/private"
98
+ end
99
+ should_fail_authentication :invalid_token
100
+ end
101
+
102
+ context "revoked HTTP token" do
103
+ setup do
104
+ Rack::OAuth2::Server::AccessToken.from_token(@token).revoke!
105
+ with_token
106
+ get "/private"
107
+ end
108
+ should_fail_authentication :invalid_token
109
+ end
110
+
111
+ context "revoked client" do
112
+ setup do
113
+ client.revoke!
114
+ with_token
115
+ get "/private"
116
+ end
117
+ should_fail_authentication :invalid_token
118
+ end
119
+ end
120
+
121
+ # 5.1.2. URI Query Parameter
122
+
123
+ context "query parameter" do
124
+ context "valid token" do
125
+ setup { get "/private?oauth_token=#{@token}" }
126
+ should_return_resource "Shhhh"
127
+ end
128
+
129
+ context "invalid token" do
130
+ setup { get "/private?oauth_token=dingdong" }
131
+ should_fail_authentication :invalid_token
132
+ end
133
+ end
134
+ end
135
+
136
+ context "POST" do
137
+ context "no authorization" do
138
+ setup { post "/change" }
139
+ should_fail_authentication
140
+ end
141
+
142
+ context "HTTP authentication" do
143
+ context "valid token" do
144
+ setup do
145
+ with_token
146
+ post "/change"
147
+ end
148
+ should_return_resource "Woot!"
149
+ end
150
+
151
+ context "unknown token" do
152
+ setup do
153
+ with_token "dingdong"
154
+ post "/change"
155
+ end
156
+ should_fail_authentication :invalid_token
157
+ end
158
+
159
+ end
160
+
161
+ # 5.1.3. Form-Encoded Body Parameter
162
+
163
+ context "body parameter" do
164
+ context "valid token" do
165
+ setup { post "/change", :oauth_token=>@token }
166
+ should_return_resource "Woot!"
167
+ end
168
+
169
+ context "invalid token" do
170
+ setup { post "/change", :oauth_token=>"dingdong" }
171
+ should_fail_authentication :invalid_token
172
+ end
173
+ end
174
+ end
175
+
176
+
177
+ context "insufficient scope" do
178
+ context "valid token" do
179
+ setup { get "/calc?oauth_token=#@token" }
180
+
181
+ should "respond with status 403 (Forbidden)" do
182
+ assert_equal 403, last_response.status
183
+ end
184
+ should "respond with authentication method OAuth" do
185
+ assert_equal "OAuth", last_response["WWW-Authenticate"].split.first
186
+ end
187
+ should "respond with realm" do
188
+ assert_match " realm=\"example.org\"", last_response["WWW-Authenticate"]
189
+ end
190
+ should "respond with error code insufficient_scope" do
191
+ assert_match " error=\"insufficient_scope\"", last_response["WWW-Authenticate"]
192
+ end
193
+ should "respond with scope name" do
194
+ assert_match " scope=\"math\"", last_response["WWW-Authenticate"]
195
+ end
196
+ end
197
+ end
198
+
199
+
200
+ context "setting resource" do
201
+ context "authenticated" do
202
+ setup do
203
+ with_token
204
+ get "/user"
205
+ end
206
+
207
+ should "render user name" do
208
+ assert_equal "Superman", last_response.body
209
+ end
210
+ end
211
+
212
+ context "not authenticated" do
213
+ setup do
214
+ get "/user"
215
+ end
216
+
217
+ should "not render user name" do
218
+ assert last_response.body.empty?
219
+ end
220
+ end
221
+ end
222
+
223
+ context "list tokens" do
224
+ setup do
225
+ @other = Rack::OAuth2::Server::AccessToken.get_token_for("foobar", "read", client.id).token
226
+ get "/list_tokens"
227
+ end
228
+
229
+ should "return access token" do
230
+ assert_contains last_response.body.split, @token
231
+ end
232
+
233
+ should "not return other resource's token" do
234
+ assert !last_response.body.split.include?(@other)
235
+ end
236
+ end
237
+ end