rack-oauth2-server 1.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -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