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.
data/Rakefile ADDED
@@ -0,0 +1,60 @@
1
+ require "rake/testtask"
2
+ require "yard"
3
+
4
+ spec = Gem::Specification.load(Dir["*.gemspec"].first)
5
+
6
+ desc "Build the Gem"
7
+ task :build do
8
+ sh "gem build #{spec.name}.gemspec"
9
+ end
10
+
11
+ desc "Install #{spec.name} locally"
12
+ task :install=>:build do
13
+ sudo = "sudo" unless File.writable?( Gem::ConfigMap[:bindir])
14
+ sh "#{sudo} gem install #{spec.name}-#{spec.version}.gem"
15
+ end
16
+
17
+ desc "Push new release to gemcutter and git tag"
18
+ task :push=>["test:all", "build"] do
19
+ sh "git push"
20
+ puts "Tagging version #{spec.version} .."
21
+ sh "git tag v#{spec.version}"
22
+ sh "git push --tag"
23
+ puts "Building and pushing gem .."
24
+ sh "gem push #{spec.name}-#{spec.version}.gem"
25
+ end
26
+
27
+ desc "Run all tests"
28
+ Rake::TestTask.new do |task|
29
+ task.test_files = FileList['test/*_test.rb']
30
+ if Rake.application.options.trace
31
+ #task.warning = true
32
+ task.verbose = true
33
+ elsif Rake.application.options.silent
34
+ task.ruby_opts << "-W0"
35
+ else
36
+ task.verbose = true
37
+ end
38
+ task.ruby_opts << "-I."
39
+ end
40
+
41
+ namespace :test do
42
+ task :all=>["test:sinatra", "test:rails2"]
43
+ desc "Run all tests against Sinatra"
44
+ task :sinatra do
45
+ sh "rake test FRAMEWORK=sinatra"
46
+ end
47
+ desc "Run all tests against Rails 2.3.x"
48
+ task :rails2 do
49
+ sh "rake test FRAMEWORK=rails2"
50
+ end
51
+ end
52
+ task :default=>"test:all"
53
+
54
+ YARD::Rake::YardocTask.new do |doc|
55
+ doc.files = FileList["lib/**/*.rb"]
56
+ end
57
+
58
+ task :clobber do
59
+ rm_rf %w{doc .yardoc *.gem}
60
+ end
@@ -0,0 +1 @@
1
+ require "rack/oauth2/server"
@@ -0,0 +1,37 @@
1
+ require "mongo"
2
+ require "openssl"
3
+
4
+
5
+ require "rack/oauth2/models/client"
6
+ require "rack/oauth2/models/auth_request"
7
+ require "rack/oauth2/models/access_grant"
8
+ require "rack/oauth2/models/access_token"
9
+
10
+ module Rack
11
+ module OAuth2
12
+ class Server
13
+
14
+ class << self
15
+ # A Mongo::DB object.
16
+ attr_accessor :database
17
+
18
+ # Create new instance of the klass and populate its attributes.
19
+ def new_instance(klass, fields)
20
+ return unless fields
21
+ instance = klass.new
22
+ fields.each do |name, value|
23
+ instance.instance_variable_set :"@#{name}", value
24
+ end
25
+ instance
26
+ end
27
+
28
+ # Long, random and hexy.
29
+ def secure_random
30
+ OpenSSL::Random.random_bytes(32).unpack("H*")[0]
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,75 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ # The access grant is a nonce, new grant created each time we need it and
6
+ # good for redeeming one access token.
7
+ class AccessGrant
8
+ class << self
9
+ # Find AccessGrant from authentication code.
10
+ def from_code(code)
11
+ Server.new_instance self, collection.find_one({ :_id=>code, :revoked=>nil })
12
+ end
13
+
14
+ # Create a new access grant.
15
+ def create(resource, scope, client_id, redirect_uri)
16
+ fields = { :_id=>Server.secure_random, :resource=>resource, :scope=>scope, :client_id=>client_id, :redirect_uri=>redirect_uri,
17
+ :created_at=>Time.now.utc, :granted_at=>nil, :access_token=>nil, :revoked=>nil }
18
+ collection.insert fields
19
+ Server.new_instance self, fields
20
+ end
21
+
22
+ def collection
23
+ Server.database["oauth2.access_grants"]
24
+ end
25
+ end
26
+
27
+ # Authorization code. We are nothing without it.
28
+ attr_reader :_id
29
+ alias :code :_id
30
+ # The resource we authorized access to.
31
+ attr_reader :resource
32
+ # Client that was granted this access token.
33
+ attr_reader :client_id
34
+ # Redirect URI for this grant.
35
+ attr_reader :redirect_uri
36
+ # The scope granted in this token.
37
+ attr_reader :scope
38
+ # Does what it says on the label.
39
+ attr_reader :created_at
40
+ # Tells us when (and if) access token was created.
41
+ attr_accessor :granted_at
42
+ # Access token created from this grant. Set and spent.
43
+ attr_accessor :access_token
44
+ # Timestamp if revoked.
45
+ attr_accessor :revoked
46
+
47
+ # Authorize access and return new access token.
48
+ #
49
+ # Access grant can only be redeemed once, but client can make multiple
50
+ # requests to obtain it, so we need to make sure only first request is
51
+ # successful in returning access token, futher requests raise
52
+ # InvalidGrantError.
53
+ def authorize!
54
+ raise InvalidGrantError if self.access_token || self.revoked
55
+ access_token = AccessToken.get_token_for(resource, scope, client_id)
56
+ self.access_token = access_token.token
57
+ self.granted_at = Time.now.utc
58
+ self.class.collection.update({ :_id=>code, :access_token=>nil, :revoked=>nil }, { :$set=>{ :granted_at=>granted_at, :access_token=>access_token.token } }, :safe=>true)
59
+ reload = self.class.collection.find_one({ :_id=>code, :revoked=>nil }, { :fields=>%w{access_token} })
60
+ raise InvalidGrantError unless reload && reload["access_token"] == access_token.token
61
+ return access_token
62
+ end
63
+
64
+ def revoke!
65
+ self.class.collection.update({ :_id=>code, :revoked=>nil }, { :$set=>{ :revoked=>Time.now.utc } })
66
+ end
67
+
68
+ # Allows us to kill all pending grants on behalf of client/resource.
69
+ #collection.create_index [[:client_id, Mongo::ASCENDING]]
70
+ #collection.create_index [[:resource, Mongo::ASCENDING]]
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,65 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ # Access token. This is what clients use to access resources.
6
+ #
7
+ # An access token is a unique code, associated with a client, a resource
8
+ # and scope. It may be revoked, or expire after a certain period.
9
+ class AccessToken
10
+ class << self
11
+ # Find AccessToken from token. Does not return revoked tokens.
12
+ def from_token(token)
13
+ Server.new_instance self, collection.find_one({ :_id=>token, :revoked=>nil })
14
+ end
15
+
16
+ # Get an access token (create new one if necessary).
17
+ def get_token_for(resource, scope, client_id)
18
+ unless token = collection.find_one({ :resource=>resource, :scope=>scope, :client_id=>client_id })
19
+ token = { :_id=>Server.secure_random, :resource=>resource, :scope=>scope, :client_id=>client_id,
20
+ :created_at=>Time.now.utc, :expires_at=>nil, :revoked=>nil }
21
+ collection.insert token
22
+ end
23
+ Server.new_instance self, token
24
+ end
25
+
26
+ # Find all AccessTokens for a resource.
27
+ def from_resource(resource)
28
+ collection.find({ :resource=>resource }).map { |fields| Server.new_instance self, fields }
29
+ end
30
+
31
+ def collection
32
+ Server.database["oauth2.access_tokens"]
33
+ end
34
+ end
35
+
36
+ # Access token. As unique as they come.
37
+ attr_reader :_id
38
+ alias :token :_id
39
+ # The resource we authorized access to.
40
+ attr_reader :resource
41
+ # Client that was granted this access token.
42
+ attr_reader :client_id
43
+ # The scope granted in this token.
44
+ attr_reader :scope
45
+ # When token was granted.
46
+ attr_reader :created_at
47
+ # When token expires for good.
48
+ attr_reader :expires_at
49
+ # Timestamp if revoked.
50
+ attr_accessor :revoked
51
+
52
+ # Revokes this access token.
53
+ def revoke!
54
+ self.revoked = Time.now.utc
55
+ AccessToken.collection.update({ :_id=>token }, { :$set=>{ :revoked=>revoked } })
56
+ end
57
+
58
+ # Allows us to kill all pending grants on behalf of client/resource.
59
+ #collection.create_index [[:client_id, Mongo::ASCENDING]]
60
+ #collection.create_index [[:resource, Mongo::ASCENDING]]
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,88 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ # Authorization request. Represents request on behalf of client to access
6
+ # particular scope. Use this to keep state from incoming authorization
7
+ # request to grant/deny redirect.
8
+ class AuthRequest
9
+ class << self
10
+ # Find AuthRequest from identifier.
11
+ def find(request_id)
12
+ id = BSON::ObjectId(request_id.to_s)
13
+ Server.new_instance self, collection.find_one(id)
14
+ end
15
+
16
+ # Create a new authorization request. This holds state, so in addition
17
+ # to client ID and scope, we need to know the URL to redirect back to
18
+ # and any state value to pass back in that redirect.
19
+ def create(client_id, scope, redirect_uri, response_type, state)
20
+ fields = { :client_id=>client_id, :scope=>scope, :redirect_uri=>redirect_uri, :state=>state,
21
+ :response_type=>response_type, :created_at=>Time.now.utc, :grant_code=>nil, :authorized_at=>nil, :revoked=>nil }
22
+ fields[:_id] = collection.insert(fields)
23
+ Server.new_instance self, fields
24
+ end
25
+
26
+ def collection
27
+ Server.database["oauth2.auth_requests"]
28
+ end
29
+ end
30
+
31
+ # Request identifier. We let the database pick this one out.
32
+ attr_reader :_id
33
+ alias :id :_id
34
+ # Client making this request.
35
+ attr_reader :client_id
36
+ # Scope of this request: array of names.
37
+ attr_reader :scope
38
+ # Redirect back to this URL.
39
+ attr_reader :redirect_uri
40
+ # Client requested we return state on redirect.
41
+ attr_reader :state
42
+ # Does what it says on the label.
43
+ attr_reader :created_at
44
+ # Response type: either code or token.
45
+ attr_reader :response_type
46
+ # If granted, the access grant code.
47
+ attr_accessor :grant_code
48
+ # If granted, the access token.
49
+ attr_accessor :access_token
50
+ # Keeping track of things.
51
+ attr_accessor :authorized_at
52
+ # Timestamp if revoked.
53
+ attr_accessor :revoked
54
+
55
+ # Grant access to the specified resource.
56
+ def grant!(resource)
57
+ raise ArgumentError, "Must supply a resource" unless resource
58
+ return if revoked
59
+ self.authorized_at = Time.now.utc
60
+ if response_type == "code" # Requested authorization code
61
+ unless self.grant_code
62
+ access_grant = AccessGrant.create(resource, scope, client_id, redirect_uri)
63
+ self.grant_code = access_grant.code
64
+ self.class.collection.update({ :_id=>id, :revoked=>nil }, { :$set=>{ :grant_code=>access_grant.code, :authorized_at=>authorized_at } })
65
+ end
66
+ else # Requested access token
67
+ unless self.access_token
68
+ access_token = AccessToken.get_token_for(resource, scope, client_id)
69
+ self.access_token = access_token.token
70
+ self.class.collection.update({ :_id=>id, :revoked=>nil, :access_token=>nil }, { :$set=>{ :access_token=>access_token.token, :authorized_at=>authorized_at } })
71
+ end
72
+ end
73
+ true
74
+ end
75
+
76
+ # Deny access.
77
+ def deny!
78
+ self.authorized_at = Time.now.utc
79
+ self.class.collection.update({ :_id=>id }, { :$set=>{ :authorized_at=>authorized_at } })
80
+ end
81
+
82
+ # Allows us to kill all pending request on behalf of client.
83
+ #collection.create_index [[:client_id, Mongo::ASCENDING]]
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,73 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ class Client
6
+
7
+ class << self
8
+ # Authenticate a client request. This method takes three arguments,
9
+ # Find Client from client identifier.
10
+ def find(client_id)
11
+ id = BSON::ObjectId(client_id.to_s)
12
+ Server.new_instance self, collection.find_one(id)
13
+ end
14
+
15
+ # Create a new client. Client provides the following properties:
16
+ # # :display_name -- Name to show (e.g. UberClient)
17
+ # # :link -- Link to client Web site (e.g. http://uberclient.dot)
18
+ # # :image_url -- URL of image to show alongside display name
19
+ # # :redirect_uri -- Registered redirect URI.
20
+ #
21
+ # This method does not validate any of these fields, in fact, you're
22
+ # not required to set them, use them, or use them as suggested. Using
23
+ # them as suggested would result in better user experience. Don't ask
24
+ # how we learned that.
25
+ def create(args)
26
+ redirect_uri = Server::Utils.parse_redirect_uri(args[:redirect_uri]).to_s if args[:redirect_uri]
27
+ fields = { :secret=>Server.secure_random, :display_name=>args[:display_name], :link=>args[:link],
28
+ :image_url=>args[:image_url], :redirect_uri=>redirect_uri, :created_at=>Time.now.utc, :revoked=>nil }
29
+ fields[:_id] = collection.insert(fields)
30
+ Server.new_instance self, fields
31
+ end
32
+
33
+ def collection
34
+ Server.database["oauth2.clients"]
35
+ end
36
+ end
37
+
38
+ # Client identifier.
39
+ attr_reader :_id
40
+ alias :id :_id
41
+ # Client secret: random, long, and hexy.
42
+ attr_reader :secret
43
+ # User see this.
44
+ attr_reader :display_name
45
+ # Link to client's Web site.
46
+ attr_reader :link
47
+ # Preferred image URL for this icon.
48
+ attr_reader :image_url
49
+ # Redirect URL. Supplied by the client if they want to restrict redirect
50
+ # URLs (better security).
51
+ attr_reader :redirect_uri
52
+ # Does what it says on the label.
53
+ attr_reader :created_at
54
+ # Timestamp if revoked.
55
+ attr_accessor :revoked
56
+
57
+ # Revoke all authorization requests, access grants and access tokens for
58
+ # this client. Ward off the evil.
59
+ def revoke!
60
+ self.revoked = Time.now.utc
61
+ Client.collection.update({ :_id=>id }, { :$set=>{ :revoked=>revoked } })
62
+ AuthRequest.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } })
63
+ AccessGrant.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } })
64
+ AccessToken.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } })
65
+ end
66
+
67
+ #collection.create_index [[:display_name, Mongo::ASCENDING]]
68
+ #collection.create_index [[:link, Mongo::ASCENDING]]
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,105 @@
1
+ require "rack/oauth2/server"
2
+
3
+ module Rack
4
+ module OAuth2
5
+
6
+ # Rails support.
7
+ #
8
+ # Adds oauth instance method that returns Rack::OAuth2::Helper, see there for
9
+ # more details.
10
+ #
11
+ # Adds oauth_required filter method. Use this filter with actions that require
12
+ # authentication, and with actions 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 config/environment.rb
19
+ # require "rack/oauth2/rails"
20
+ #
21
+ # Rails::Initializer.run do |config|
22
+ # config.oauth[:scopes] = %w{read write}
23
+ # config.oauth[:authenticator] = lambda do |username, password|
24
+ # User.authenticated username, password
25
+ # end
26
+ # . . .
27
+ # end
28
+ #
29
+ # @example app/controllers/my_controller.rb
30
+ # class MyController < ApplicationController
31
+ #
32
+ # oauth_required :only=>:show
33
+ # oauth_required :only=>:update, :scope=>"write"
34
+ #
35
+ # . . .
36
+ #
37
+ # protected
38
+ # def current_user
39
+ # @current_user ||= User.find(oauth.resource) if oauth.authenticated?
40
+ # end
41
+ # end
42
+ #
43
+ # @see Helpers
44
+ # @see Filters
45
+ # @see Configuration
46
+ module Rails
47
+
48
+ # Helper methods available to controller instance and views.
49
+ module Helpers
50
+ # Returns the OAuth helper.
51
+ #
52
+ # @return [Server::Helper]
53
+ def oauth
54
+ @oauth ||= Rack::OAuth2::Server::Helper.new(request, response)
55
+ end
56
+ end
57
+
58
+ # Filter methods available in controller.
59
+ module Filters
60
+
61
+ # Adds before filter to require authentication on all the listed paths.
62
+ # Use the :scope option if client must also have access to that scope.
63
+ #
64
+ # @param [Hash] options Accepts before_filter options like :only and
65
+ # :except, and the :scope option.
66
+ def oauth_required(options = {})
67
+ scope = options.delete(:scope)
68
+ before_filter options do |controller|
69
+ if controller.oauth.authenticated?
70
+ if scope && !controller.oauth.scope.include?(scope)
71
+ controller.send :head, controller.oauth.no_scope!(scope)
72
+ end
73
+ else
74
+ controller.send :head, controller.oauth.no_access!
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ # Configuration methods available in config/environment.rb.
81
+ module Configuration
82
+
83
+ # Rack module settings.
84
+ #
85
+ # @return [Hash] Settings
86
+ def oauth
87
+ @oauth ||= { :logger=>::Rails.logger }
88
+ end
89
+ end
90
+
91
+ end
92
+
93
+ end
94
+ end
95
+
96
+ class Rails::Configuration
97
+ include Rack::OAuth2::Rails::Configuration
98
+ end
99
+ class ActionController::Base
100
+ helper Rack::OAuth2::Rails::Helpers
101
+ include Rack::OAuth2::Rails::Helpers
102
+ extend Rack::OAuth2::Rails::Filters
103
+ end
104
+ # Add middleware now, but load configuration as late as possible.
105
+ ActionController::Dispatcher.middleware.use Rack::OAuth2::Server, lambda { Rails.configuration.oauth }