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

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