rack-oauth2-server 2.0.0.beta4 → 2.0.0.beta5

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/CHANGELOG CHANGED
@@ -1,4 +1,4 @@
1
- 2010-11-16 version 2.0.0
1
+ 2010-11-18 version 2.0.0
2
2
 
3
3
  MAJOR CHANGE:
4
4
  Keeping with OAuth 2.0 spec terminology, we'll call it scope all around. Some
data/README.rdoc CHANGED
@@ -332,7 +332,7 @@ I'll let you figure that one for yourself.
332
332
 
333
333
  We haz it, and it's pretty rad:
334
334
 
335
- http://github.com/downloads/flowtown/rack-oauth2-server/OAuth%20Console%20-%20All%20Clients.png
335
+ http://labnotes.org/wp-content/uploads/2010/11/OAuth-Admin-All-Clients.png
336
336
 
337
337
  To get the Web admin running, you'll need to do the following. First, you'll
338
338
  need to register a new client application that can access the OAuth Web admin,
@@ -479,6 +479,30 @@ server you +curl+. Useful for development, testing, just don't use it with any
479
479
  production access tokens.
480
480
 
481
481
 
482
+ == Methods You'll Want To Use From Your App
483
+
484
+ You can use the Server module to create, fetch and otherwise work with access
485
+ tokens and grants. Available methods include:
486
+
487
+ - access_grant -- Creates and returns a new access grant. You can use that for
488
+ one-time token, e.g. users who forgot their password and need to login using
489
+ an email message.
490
+ - token_for -- Returns access token for particular identity. You can use that to
491
+ give access tokens to clients other than through the OAuth 2.0 protocol, e.g.
492
+ if you let users authenticate using Facebook Connect or Twitter OAuth.
493
+ - get_access_token -- Resolves access token (string) into access token
494
+ (AccessToken object).
495
+ - list_access_tokens -- Returns all access tokens for a given identity, which
496
+ you'll need if you offer a UI for uses to review and revoke access tokens they
497
+ previously granted.
498
+ - get_client -- Resolves client identifier into a Client object.
499
+ - register -- Registers a new client application. Can also be used to change
500
+ existing registration (if you know the client's ID and secret). Idempotent, so
501
+ perfect for running during setup and migration.
502
+ - get_auth_request -- Resolves authorization request handle into an AuthRequest
503
+ object. Could be useful during the authorization flow.
504
+
505
+
482
506
  == Mandatory ASCII Diagram
483
507
 
484
508
  This is briefly what the authorization flow looks like, how the workload is
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.0.beta4
1
+ 2.0.0.beta5
@@ -12,11 +12,13 @@ module Rack
12
12
  end
13
13
 
14
14
  # Create a new access grant.
15
- def create(identity, client, scope, redirect_uri = nil)
15
+ def create(identity, client, scope, redirect_uri = nil, expires = nil)
16
16
  scope = Utils.normalize_scope(scope) & client.scope # Only allowed scope
17
+ expires_at = Time.now.to_i + (expires || 300)
17
18
  fields = { :_id=>Server.secure_random, :identity=>identity.to_s, :scope=>scope,
18
19
  :client_id=>client.id, :redirect_uri=>client.redirect_uri || redirect_uri,
19
- :created_at=>Time.now.utc.to_i, :granted_at=>nil, :access_token=>nil, :revoked=>nil }
20
+ :created_at=>Time.now.to_i, :expires_at=>expires_at, :granted_at=>nil,
21
+ :access_token=>nil, :revoked=>nil }
20
22
  collection.insert fields
21
23
  Server.new_instance self, fields
22
24
  end
@@ -41,6 +43,8 @@ module Rack
41
43
  attr_reader :created_at
42
44
  # Tells us when (and if) access token was created.
43
45
  attr_accessor :granted_at
46
+ # Tells us when this grant expires.
47
+ attr_accessor :expires_at
44
48
  # Access token created from this grant. Set and spent.
45
49
  attr_accessor :access_token
46
50
  # Timestamp if revoked.
@@ -57,7 +61,7 @@ module Rack
57
61
  client = Client.find(client_id) or raise InvalidGrantError
58
62
  access_token = AccessToken.get_token_for(identity, client, scope)
59
63
  self.access_token = access_token.token
60
- self.granted_at = Time.now.utc.to_i
64
+ self.granted_at = Time.now.to_i
61
65
  self.class.collection.update({ :_id=>code, :access_token=>nil, :revoked=>nil }, { :$set=>{ :granted_at=>granted_at, :access_token=>access_token.token } }, :safe=>true)
62
66
  reload = self.class.collection.find_one({ :_id=>code, :revoked=>nil }, { :fields=>%w{access_token} })
63
67
  raise InvalidGrantError unless reload && reload["access_token"] == access_token.token
@@ -65,7 +69,7 @@ module Rack
65
69
  end
66
70
 
67
71
  def revoke!
68
- self.revoked = Time.now.utc.to_i
72
+ self.revoked = Time.now.to_i
69
73
  self.class.collection.update({ :_id=>code, :revoked=>nil }, { :$set=>{ :revoked=>revoked } })
70
74
  end
71
75
 
@@ -19,7 +19,7 @@ module Rack
19
19
  scope = Utils.normalize_scope(scope) & client.scope # Only allowed scope
20
20
  unless token = collection.find_one({ :identity=>identity.to_s, :scope=>scope, :client_id=>client.id, :revoked=>nil })
21
21
  token = { :_id=>Server.secure_random, :identity=>identity.to_s, :scope=>scope,
22
- :client_id=>client.id, :created_at=>Time.now.utc.to_i,
22
+ :client_id=>client.id, :created_at=>Time.now.to_i,
23
23
  :expires_at=>nil, :revoked=>nil }
24
24
  collection.insert token
25
25
  end
@@ -48,7 +48,7 @@ module Rack
48
48
  def count(filter = {})
49
49
  select = {}
50
50
  if filter[:days]
51
- now = Time.now.utc.to_i
51
+ now = Time.now.to_i
52
52
  range = { :$gt=>now - filter[:days] * 86400, :$lte=>now }
53
53
  select[ filter[:revoked] ? :revoked : :created_at ] = range
54
54
  elsif filter.has_key?(:revoked)
@@ -93,7 +93,7 @@ module Rack
93
93
 
94
94
  # Revokes this access token.
95
95
  def revoke!
96
- self.revoked = Time.now.utc.to_i
96
+ self.revoked = Time.now.to_i
97
97
  AccessToken.collection.update({ :_id=>token }, { :$set=>{ :revoked=>revoked } })
98
98
  end
99
99
 
@@ -21,7 +21,7 @@ module Rack
21
21
  fields = { :client_id=>client.id, :scope=>scope, :redirect_uri=>client.redirect_uri || redirect_uri,
22
22
  :response_type=>response_type, :state=>state,
23
23
  :grant_code=>nil, :authorized_at=>nil,
24
- :created_at=>Time.now.utc.to_i, :revoked=>nil }
24
+ :created_at=>Time.now.to_i, :revoked=>nil }
25
25
  fields[:_id] = collection.insert(fields)
26
26
  Server.new_instance self, fields
27
27
  end
@@ -60,7 +60,7 @@ module Rack
60
60
  raise ArgumentError, "Must supply a identity" unless identity
61
61
  return if revoked
62
62
  client = Client.find(client_id) or return
63
- self.authorized_at = Time.now.utc.to_i
63
+ self.authorized_at = Time.now.to_i
64
64
  if response_type == "code" # Requested authorization code
65
65
  access_grant = AccessGrant.create(identity, client, scope, redirect_uri)
66
66
  self.grant_code = access_grant.code
@@ -75,7 +75,7 @@ module Rack
75
75
 
76
76
  # Deny access.
77
77
  def deny!
78
- self.authorized_at = Time.now.utc.to_i
78
+ self.authorized_at = Time.now.to_i
79
79
  self.class.collection.update({ :_id=>id }, { :$set=>{ :authorized_at=>authorized_at } })
80
80
  end
81
81
 
@@ -30,7 +30,7 @@ module Rack
30
30
  fields = { :display_name=>args[:display_name], :link=>args[:link],
31
31
  :image_url=>args[:image_url], :redirect_uri=>redirect_uri,
32
32
  :nodes=>args[:notes].to_s, :scope=>scope,
33
- :created_at=>Time.now.utc.to_i, :revoked=>nil }
33
+ :created_at=>Time.now.to_i, :revoked=>nil }
34
34
  if args[:id] && args[:secret]
35
35
  fields[:_id], fields[:secret] = BSON::ObjectId(args[:id].to_s), args[:secret]
36
36
  collection.insert(fields, :safe=>true)
@@ -95,7 +95,7 @@ module Rack
95
95
  # Revoke all authorization requests, access grants and access tokens for
96
96
  # this client. Ward off the evil.
97
97
  def revoke!
98
- self.revoked = Time.now.utc.to_i
98
+ self.revoked = Time.now.to_i
99
99
  Client.collection.update({ :_id=>id }, { :$set=>{ :revoked=>revoked } })
100
100
  AuthRequest.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } })
101
101
  AccessGrant.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } })
@@ -42,8 +42,8 @@ module Rack
42
42
  # assertion, expired authorization token, bad end-user password credentials,
43
43
  # or mismatching authorization code and redirection URI).
44
44
  class InvalidGrantError < OAuthError
45
- def initialize
46
- super :invalid_grant, "This access grant is no longer valid."
45
+ def initialize(message)
46
+ super :invalid_grant, message || "This access grant is no longer valid."
47
47
  end
48
48
  end
49
49
 
@@ -8,11 +8,10 @@ module Rack
8
8
  # Rails 3.x integration.
9
9
  class Railtie < ::Rails::Railtie # :nodoc:
10
10
  config.oauth = Server::Options.new
11
- config.oauth.logger = ::Rails.logger
12
11
 
13
12
  initializer "rack-oauth2-server" do |app|
14
- #app.config.extend ::Rack::OAuth2::Rails::Configuration
15
13
  app.middleware.use ::Rack::OAuth2::Server, app.config.oauth
14
+ config.oauth.logger ||= ::Rails.logger
16
15
  class ::ActionController::Base
17
16
  helper ::Rack::OAuth2::Rails::Helpers
18
17
  include ::Rack::OAuth2::Rails::Helpers
@@ -79,10 +79,12 @@ module Rack
79
79
  # @param [String] identity User ID, account ID, etc
80
80
  # @param [String] client_id Client identifier
81
81
  # @param [Array, nil] scope Array of string, nil if you want 'em all
82
+ # @param [Integer, nil] expires How many seconds before access grant
83
+ # expires (default to 5 minutes)
82
84
  # @return [String] Access grant authorization code
83
- def access_grant(identity, client_id, scope = nil)
85
+ def access_grant(identity, client_id, scope = nil, expires = nil)
84
86
  client = get_client(client_id) or fail "No such client"
85
- AccessGrant.create(identity, client, scope || client.scope).code
87
+ AccessGrant.create(identity, client, scope || client.scope, nil, expires).code
86
88
  end
87
89
 
88
90
  # Returns AccessToken from token.
@@ -191,16 +193,16 @@ module Rack
191
193
  begin
192
194
  access_token = AccessToken.from_token(token)
193
195
  raise InvalidTokenError if access_token.nil? || access_token.revoked
194
- raise ExpiredTokenError if access_token.expires_at && access_token.expires_at <= Time.now.utc
196
+ raise ExpiredTokenError if access_token.expires_at && access_token.expires_at <= Time.now.to_i
195
197
  request.env["oauth.access_token"] = token
196
198
  request.env["oauth.identity"] = access_token.identity
197
- logger.info "Authorized #{access_token.identity}" if logger
199
+ logger.info "RO2S: Authorized #{access_token.identity}" if logger
198
200
  rescue OAuthError=>error
199
201
  # 5.2. The WWW-Authenticate Response Header Field
200
- logger.info "HTTP authorization failed #{error.code}" if logger
202
+ logger.info "RO2S: HTTP authorization failed #{error.code}" if logger
201
203
  return unauthorized(request, error)
202
204
  rescue =>ex
203
- logger.info "HTTP authorization failed #{ex.message}" if logger
205
+ logger.info "RO2S: HTTP authorization failed #{ex.message}" if logger
204
206
  return unauthorized(request)
205
207
  end
206
208
 
@@ -246,13 +248,13 @@ module Rack
246
248
  if request.GET["authorization"]
247
249
  auth_request = self.class.get_auth_request(request.GET["authorization"]) rescue nil
248
250
  if !auth_request || auth_request.revoked
249
- logger.error "Invalid authorization request #{auth_request}" if logger
251
+ logger.error "RO2S: Invalid authorization request #{auth_request}" if logger
250
252
  return bad_request("Invalid authorization request")
251
253
  end
252
254
  response_type = auth_request.response_type # Needed for error handling
253
255
  client = self.class.get_client(auth_request.client_id)
254
256
  # Pass back to application, watch for 403 (deny!)
255
- logger.info "Request #{auth_request.id}: Client #{client.display_name} requested #{auth_request.response_type} with scope #{auth_request.scope.join(" ")}" if logger
257
+ logger.info "RO2S: Client #{client.display_name} requested #{auth_request.response_type} with scope #{auth_request.scope.join(" ")}" if logger
256
258
  request.env["oauth.authorization"] = auth_request.id.to_s
257
259
  response = @app.call(request.env)
258
260
  raise AccessDeniedError if response[0] == 403
@@ -264,7 +266,7 @@ module Rack
264
266
  begin
265
267
  redirect_uri = Utils.parse_redirect_uri(request.GET["redirect_uri"])
266
268
  rescue InvalidRequestError=>error
267
- logger.error "Authorization request with invalid redirect_uri: #{request.GET["redirect_uri"]} #{error.message}" if logger
269
+ logger.error "RO2S: Authorization request with invalid redirect_uri: #{request.GET["redirect_uri"]} #{error.message}" if logger
268
270
  return bad_request(error.message)
269
271
  end
270
272
 
@@ -284,7 +286,7 @@ module Rack
284
286
  return [303, { "Location"=>uri.to_s }, ["You are being redirected"]]
285
287
  end
286
288
  rescue OAuthError=>error
287
- logger.error "Authorization request error: #{error.code} #{error.message}" if logger
289
+ logger.error "RO2S: Authorization request error #{error.code}: #{error.message}" if logger
288
290
  params = { :error=>error.code, :error_description=>error.message, :state=>state }
289
291
  if response_type == "token"
290
292
  redirect_uri.fragment = Rack::Utils.build_query(params)
@@ -311,20 +313,20 @@ module Rack
311
313
  # 3.1. Authorization Response
312
314
  if auth_request.response_type == "code"
313
315
  if auth_request.grant_code
314
- logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} granted access code #{auth_request.grant_code}" if logger
316
+ logger.info "RO2S: Client #{auth_request.client_id} granted access code #{auth_request.grant_code}" if logger
315
317
  params = { :code=>auth_request.grant_code, :scope=>auth_request.scope.join(" "), :state=>auth_request.state }
316
318
  else
317
- logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} denied authorization" if logger
319
+ logger.info "RO2S: Client #{auth_request.client_id} denied authorization" if logger
318
320
  params = { :error=>:access_denied, :state=>auth_request.state }
319
321
  end
320
322
  params = Rack::Utils.parse_query(redirect_uri.query).merge(params)
321
323
  redirect_uri.query = Rack::Utils.build_query(params)
322
324
  else # response type if token
323
325
  if auth_request.access_token
324
- logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} granted access token #{auth_request.access_token}" if logger
326
+ logger.info "RO2S: Client #{auth_request.client_id} granted access token #{auth_request.access_token}" if logger
325
327
  params = { :access_token=>auth_request.access_token, :scope=>auth_request.scope.join(" "), :state=>auth_request.state }
326
328
  else
327
- logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} denied authorization" if logger
329
+ logger.info "RO2S: Client #{auth_request.client_id} denied authorization" if logger
328
330
  params = { :error=>:access_denied, :state=>auth_request.state }
329
331
  end
330
332
  redirect_uri.fragment = Rack::Utils.build_query(params)
@@ -342,35 +344,36 @@ module Rack
342
344
  when "authorization_code"
343
345
  # 4.1.1. Authorization Code
344
346
  grant = AccessGrant.from_code(request.POST["code"])
345
- raise InvalidGrantError unless grant && client.id == grant.client_id
346
- raise InvalidGrantError unless grant.redirect_uri.nil? || grant.redirect_uri == Utils.parse_redirect_uri(request.POST["redirect_uri"]).to_s
347
+ raise InvalidGrantError, "Wrong client" unless grant && client.id == grant.client_id
348
+ raise InvalidGrantError, "Wrong redirect URI" unless grant.redirect_uri.nil? || grant.redirect_uri == Utils.parse_redirect_uri(request.POST["redirect_uri"]).to_s
349
+ raise InvalidGrantError, "This access grant expired" if grant.expires_at && grant.expires_at <= Time.now.to_i
347
350
  access_token = grant.authorize!
348
351
  when "password"
349
352
  raise UnsupportedGrantType unless options.authenticator
350
353
  # 4.1.2. Resource Owner Password Credentials
351
354
  username, password = request.POST.values_at("username", "password")
352
- raise InvalidGrantError unless username && password
355
+ raise InvalidGrantError, "Missing username/password" unless username && password
353
356
  requested_scope = Utils.normalize_scope(request.POST["scope"])
354
357
  allowed_scope = client.scope
355
358
  raise InvalidScopeError unless (requested_scope - allowed_scope).empty?
356
359
  args = [username, password]
357
360
  args << client.id << requested_scope unless options.authenticator.arity == 2
358
361
  identity = options.authenticator.call(*args)
359
- raise InvalidGrantError unless identity
362
+ raise InvalidGrantError, "Username/password do not match" unless identity
360
363
  access_token = AccessToken.get_token_for(identity, client, requested_scope)
361
364
  else
362
365
  raise UnsupportedGrantType
363
366
  end
364
- logger.info "Access token #{access_token.token} granted to client #{client.display_name}, identity #{access_token.identity}" if logger
367
+ logger.info "RO2S: Access token #{access_token.token} granted to client #{client.display_name}, identity #{access_token.identity}" if logger
365
368
  response = { :access_token=>access_token.token }
366
369
  response[:scope] = access_token.scope.join(" ")
367
- return [200, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" }, response.to_json]
370
+ return [200, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" }, [response.to_json]]
368
371
  # 4.3. Error Response
369
372
  rescue OAuthError=>error
370
- logger.error "Access token request error: #{error.code} #{error.message}" if logger
373
+ logger.error "RO2S: Access token request error #{error.code}: #{error.message}" if logger
371
374
  return unauthorized(request, error) if InvalidClientError === error && request.basic?
372
375
  return [400, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" },
373
- { :error=>error.code, :error_description=>error.message }.to_json]
376
+ [{ :error=>error.code, :error_description=>error.message }.to_json]]
374
377
  end
375
378
  end
376
379
 
@@ -176,6 +176,15 @@ class AccessGrantTest < Test::Unit::TestCase
176
176
  should_respond_with_access_token
177
177
  end
178
178
 
179
+ context "access grant expired" do
180
+ setup do
181
+ Timecop.travel 300 do
182
+ request_access_token
183
+ end
184
+ end
185
+ should_return_error :invalid_grant
186
+ end
187
+
179
188
 
180
189
  # 4.1.2. Resource Owner Password Credentials
181
190
 
@@ -166,6 +166,50 @@ class ServerTest < Test::Unit::TestCase
166
166
  end
167
167
  end
168
168
 
169
+ context "no expiration" do
170
+ setup do
171
+ @code = Server.access_grant("Batman", client.id)
172
+ end
173
+
174
+ should "not expire in a minute" do
175
+ Timecop.travel 60 do
176
+ basic_authorize client.id, client.secret
177
+ post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri
178
+ assert_equal 200, last_response.status
179
+ end
180
+ end
181
+
182
+ should "expire after 5 minutes" do
183
+ Timecop.travel 300 do
184
+ basic_authorize client.id, client.secret
185
+ post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri
186
+ assert_equal 400, last_response.status
187
+ end
188
+ end
189
+ end
190
+
191
+ context "expiration set" do
192
+ setup do
193
+ @code = Server.access_grant("Batman", client.id, nil, 1800)
194
+ end
195
+
196
+ should "not expire prematurely" do
197
+ Timecop.travel 1750 do
198
+ basic_authorize client.id, client.secret
199
+ post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri
200
+ assert_equal 200, last_response.status
201
+ end
202
+ end
203
+
204
+ should "expire after specified seconds" do
205
+ Timecop.travel 1800 do
206
+ basic_authorize client.id, client.secret
207
+ post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri
208
+ assert_equal 400, last_response.status
209
+ end
210
+ end
211
+ end
212
+
169
213
  end
170
214
 
171
215