oauth2-provider-jonrowe 0.1.0

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.
Files changed (45) hide show
  1. data/README.rdoc +314 -0
  2. data/example/README.rdoc +11 -0
  3. data/example/application.rb +151 -0
  4. data/example/config.ru +3 -0
  5. data/example/environment.rb +11 -0
  6. data/example/models/connection.rb +9 -0
  7. data/example/models/note.rb +4 -0
  8. data/example/models/user.rb +6 -0
  9. data/example/public/style.css +78 -0
  10. data/example/schema.rb +27 -0
  11. data/example/views/authorize.erb +28 -0
  12. data/example/views/create_user.erb +3 -0
  13. data/example/views/home.erb +25 -0
  14. data/example/views/layout.erb +25 -0
  15. data/example/views/login.erb +20 -0
  16. data/example/views/new_client.erb +25 -0
  17. data/example/views/new_user.erb +22 -0
  18. data/example/views/show_client.erb +15 -0
  19. data/lib/oauth2/model.rb +17 -0
  20. data/lib/oauth2/model/authorization.rb +113 -0
  21. data/lib/oauth2/model/client.rb +55 -0
  22. data/lib/oauth2/model/client_owner.rb +13 -0
  23. data/lib/oauth2/model/hashing.rb +27 -0
  24. data/lib/oauth2/model/resource_owner.rb +26 -0
  25. data/lib/oauth2/model/schema.rb +42 -0
  26. data/lib/oauth2/provider.rb +117 -0
  27. data/lib/oauth2/provider/access_token.rb +66 -0
  28. data/lib/oauth2/provider/authorization.rb +168 -0
  29. data/lib/oauth2/provider/error.rb +29 -0
  30. data/lib/oauth2/provider/exchange.rb +212 -0
  31. data/lib/oauth2/router.rb +60 -0
  32. data/spec/factories.rb +27 -0
  33. data/spec/oauth2/model/authorization_spec.rb +216 -0
  34. data/spec/oauth2/model/client_spec.rb +55 -0
  35. data/spec/oauth2/model/resource_owner_spec.rb +55 -0
  36. data/spec/oauth2/provider/access_token_spec.rb +125 -0
  37. data/spec/oauth2/provider/authorization_spec.rb +323 -0
  38. data/spec/oauth2/provider/exchange_spec.rb +330 -0
  39. data/spec/oauth2/provider_spec.rb +531 -0
  40. data/spec/request_helpers.rb +46 -0
  41. data/spec/spec_helper.rb +44 -0
  42. data/spec/test_app/helper.rb +33 -0
  43. data/spec/test_app/provider/application.rb +61 -0
  44. data/spec/test_app/provider/views/authorize.erb +19 -0
  45. metadata +220 -0
@@ -0,0 +1,55 @@
1
+ module OAuth2
2
+ module Model
3
+
4
+ class Client < ActiveRecord::Base
5
+ set_table_name :oauth2_clients
6
+
7
+ belongs_to :oauth2_client_owner, :polymorphic => true
8
+ alias :owner :oauth2_client_owner
9
+ alias :owner= :oauth2_client_owner=
10
+
11
+ has_many :authorizations, :class_name => 'OAuth2::Model::Authorization', :dependent => :destroy
12
+
13
+ validates_uniqueness_of :client_id
14
+ validates_presence_of :name, :redirect_uri
15
+ validate :check_format_of_redirect_uri
16
+
17
+ attr_accessible :name, :redirect_uri
18
+
19
+ before_create :generate_credentials
20
+
21
+ def self.create_client_id
22
+ OAuth2.generate_id do |client_id|
23
+ count(:conditions => {:client_id => client_id}).zero?
24
+ end
25
+ end
26
+
27
+ attr_reader :client_secret
28
+
29
+ def client_secret=(secret)
30
+ @client_secret = secret
31
+ self.client_secret_hash = BCrypt::Password.create(secret)
32
+ end
33
+
34
+ def valid_client_secret?(secret)
35
+ BCrypt::Password.new(client_secret_hash) == secret
36
+ end
37
+
38
+ private
39
+
40
+ def check_format_of_redirect_uri
41
+ uri = URI.parse(redirect_uri)
42
+ errors.add(:redirect_uri, 'must be an absolute URI') unless uri.absolute?
43
+ rescue
44
+ errors.add(:redirect_uri, 'must be a URI')
45
+ end
46
+
47
+ def generate_credentials
48
+ self.client_id = self.class.create_client_id
49
+ self.client_secret = OAuth2.random_string
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+
@@ -0,0 +1,13 @@
1
+ module OAuth2
2
+ module Model
3
+
4
+ module ClientOwner
5
+ def self.included(klass)
6
+ klass.has_many :oauth2_clients,
7
+ :class_name => 'OAuth2::Model::Client',
8
+ :as => :oauth2_client_owner
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ module OAuth2
2
+ module Model
3
+
4
+ module Hashing
5
+ def hashes_attributes(*attributes)
6
+ attributes.each do |attribute|
7
+ define_method("#{attribute}=") do |value|
8
+ instance_variable_set("@#{attribute}", value)
9
+ __send__("#{attribute}_hash=", value && OAuth2.hashify(value))
10
+ end
11
+ attr_reader attribute
12
+ end
13
+
14
+ class_eval <<-RUBY
15
+ def reload(*args)
16
+ super
17
+ #{ attributes.inspect }.each do |attribute|
18
+ instance_variable_set('@' + attribute.to_s, nil)
19
+ end
20
+ end
21
+ RUBY
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+
@@ -0,0 +1,26 @@
1
+ module OAuth2
2
+ module Model
3
+
4
+ module ResourceOwner
5
+ def self.included(klass)
6
+ klass.has_many :oauth2_authorizations,
7
+ :class_name => 'OAuth2::Model::Authorization',
8
+ :as => :oauth2_resource_owner,
9
+ :dependent => :destroy
10
+ end
11
+
12
+ def grant_access!(client, options = {})
13
+ authorization = oauth2_authorizations.find_by_client_id(client.id) ||
14
+ Model::Authorization.create(:owner => self, :client => client)
15
+
16
+ if scopes = options[:scopes]
17
+ scopes = authorization.scopes + scopes
18
+ authorization.update_attribute(:scope, scopes.join(' '))
19
+ end
20
+
21
+ authorization
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ module OAuth2
2
+ module Model
3
+
4
+ class Schema < ActiveRecord::Migration
5
+ def self.up
6
+ create_table :oauth2_clients, :force => true do |t|
7
+ t.timestamps
8
+ t.string :oauth2_client_owner_type
9
+ t.integer :oauth2_client_owner_id
10
+ t.string :name
11
+ t.string :client_id
12
+ t.string :client_secret_hash
13
+ t.string :redirect_uri
14
+ end
15
+ add_index :oauth2_clients, :client_id
16
+
17
+ create_table :oauth2_authorizations, :force => true do |t|
18
+ t.timestamps
19
+ t.string :oauth2_resource_owner_type
20
+ t.integer :oauth2_resource_owner_id
21
+ t.belongs_to :client
22
+ t.string :scope
23
+ t.string :code, :limit => 40
24
+ t.string :access_token_hash, :limit => 40
25
+ t.string :refresh_token_hash, :limit => 40
26
+ t.datetime :expires_at
27
+ end
28
+ add_index :oauth2_authorizations, [:client_id, :code]
29
+ add_index :oauth2_authorizations, [:access_token_hash]
30
+ add_index :oauth2_authorizations, [:client_id, :access_token_hash]
31
+ add_index :oauth2_authorizations, [:client_id, :refresh_token_hash]
32
+ end
33
+
34
+ def self.down
35
+ drop_table :oauth2_clients
36
+ drop_table :oauth2_authorizations
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+
@@ -0,0 +1,117 @@
1
+ require 'cgi'
2
+ require 'digest/sha1'
3
+ require 'bcrypt'
4
+ require 'json'
5
+ require 'active_record'
6
+
7
+ module OAuth2
8
+ ROOT = File.expand_path(File.dirname(__FILE__) + '/..')
9
+ TOKEN_SIZE = 128
10
+
11
+ autoload :Model, ROOT + '/oauth2/model'
12
+ autoload :Router, ROOT + '/oauth2/router'
13
+
14
+ def self.random_string
15
+ rand(2 ** TOKEN_SIZE).to_s(36)
16
+ end
17
+
18
+ def self.generate_id(&predicate)
19
+ id = random_string
20
+ id = random_string until predicate.call(id)
21
+ id
22
+ end
23
+
24
+ def self.hashify(token)
25
+ return nil unless String === token
26
+ Digest::SHA1.hexdigest(token)
27
+ end
28
+
29
+ ACCESS_TOKEN = 'access_token'
30
+ ASSERTION = 'assertion'
31
+ ASSERTION_TYPE = 'assertion_type'
32
+ AUTHORIZATION_CODE = 'authorization_code'
33
+ CLIENT_ID = 'client_id'
34
+ CLIENT_SECRET = 'client_secret'
35
+ CODE = 'code'
36
+ CODE_AND_TOKEN = 'code_and_token'
37
+ DURATION = 'duration'
38
+ ERROR = 'error'
39
+ ERROR_DESCRIPTION = 'error_description'
40
+ EXPIRES_IN = 'expires_in'
41
+ GRANT_TYPE = 'grant_type'
42
+ OAUTH_TOKEN = 'oauth_token'
43
+ PASSWORD = 'password'
44
+ REDIRECT_URI = 'redirect_uri'
45
+ REFRESH_TOKEN = 'refresh_token'
46
+ RESPONSE_TYPE = 'response_type'
47
+ SCOPE = 'scope'
48
+ STATE = 'state'
49
+ TOKEN = 'token'
50
+ USERNAME = 'username'
51
+
52
+ INVALID_REQUEST = 'invalid_request'
53
+ UNSUPPORTED_RESPONSE = 'unsupported_response_type'
54
+ REDIRECT_MISMATCH = 'redirect_uri_mismatch'
55
+ UNSUPPORTED_GRANT_TYPE = 'unsupported_grant_type'
56
+ INVALID_GRANT = 'invalid_grant'
57
+ INVALID_CLIENT = 'invalid_client'
58
+ UNAUTHORIZED_CLIENT = 'unauthorized_client'
59
+ INVALID_SCOPE = 'invalid_scope'
60
+ INVALID_TOKEN = 'invalid_token'
61
+ EXPIRED_TOKEN = 'expired_token'
62
+ INSUFFICIENT_SCOPE = 'insufficient_scope'
63
+ ACCESS_DENIED = 'access_denied'
64
+
65
+ class Provider
66
+ class << self
67
+ attr_accessor :realm, :enforce_ssl
68
+ end
69
+
70
+ def self.clear_assertion_handlers!
71
+ @password_handler = nil
72
+ @assertion_handlers = {}
73
+ @assertion_filters = []
74
+ end
75
+
76
+ clear_assertion_handlers!
77
+
78
+ def self.handle_passwords(&block)
79
+ @password_handler = block
80
+ end
81
+
82
+ def self.handle_password(client, username, password)
83
+ return nil unless @password_handler
84
+ @password_handler.call(client, username, password)
85
+ end
86
+
87
+ def self.filter_assertions(&filter)
88
+ @assertion_filters.push(filter)
89
+ end
90
+
91
+ def self.handle_assertions(assertion_type, &handler)
92
+ @assertion_handlers[assertion_type] = handler
93
+ end
94
+
95
+ def self.handle_assertion(client, assertion)
96
+ return nil unless @assertion_filters.all? { |f| f.call(client) }
97
+ handler = @assertion_handlers[assertion.type]
98
+ handler ? handler.call(client, assertion.value) : nil
99
+ end
100
+
101
+ def self.parse(*args)
102
+ Router.parse(*args)
103
+ end
104
+
105
+ def self.access_token(*args)
106
+ Router.access_token(*args)
107
+ end
108
+
109
+ EXPIRY_TIME = 3600
110
+
111
+ autoload :Authorization, ROOT + '/oauth2/provider/authorization'
112
+ autoload :Exchange, ROOT + '/oauth2/provider/exchange'
113
+ autoload :AccessToken, ROOT + '/oauth2/provider/access_token'
114
+ autoload :Error, ROOT + '/oauth2/provider/error'
115
+ end
116
+ end
117
+
@@ -0,0 +1,66 @@
1
+ module OAuth2
2
+ class Provider
3
+
4
+ class AccessToken
5
+ attr_reader :authorization
6
+
7
+ def initialize(resource_owner = nil, scopes = [], access_token = nil, error = nil)
8
+ @resource_owner = resource_owner
9
+ @scopes = scopes
10
+ @access_token = access_token
11
+ @error = error && INVALID_REQUEST
12
+
13
+ authorize!(access_token, error)
14
+ validate!
15
+ end
16
+
17
+ def client
18
+ valid? ? @authorization.client : nil
19
+ end
20
+
21
+ def owner
22
+ valid? ? @authorization.owner : nil
23
+ end
24
+
25
+ def response_headers
26
+ return {} if valid?
27
+ error_message = "OAuth realm='#{ Provider.realm }'"
28
+ error_message << ", error='#{ @error }'" unless @error == ''
29
+ {'WWW-Authenticate' => error_message}
30
+ end
31
+
32
+ def response_status
33
+ case @error
34
+ when INVALID_REQUEST, INVALID_TOKEN, EXPIRED_TOKEN then 401
35
+ when INSUFFICIENT_SCOPE then 403
36
+ when '' then 401
37
+ else 200
38
+ end
39
+ end
40
+
41
+ def valid?
42
+ @error.nil?
43
+ end
44
+
45
+ private
46
+
47
+ def authorize!(access_token, error)
48
+ return unless @authorization = Model.find_access_token(access_token)
49
+ @authorization.update_attribute(:access_token, nil) if error
50
+ end
51
+
52
+ def validate!
53
+ return @error = '' unless @access_token
54
+ return @error = INVALID_TOKEN unless @authorization
55
+ return @error = EXPIRED_TOKEN if @authorization.expired?
56
+ return @error = INSUFFICIENT_SCOPE unless @authorization.in_scope?(@scopes)
57
+
58
+ if @resource_owner and @authorization.owner != @resource_owner
59
+ @error = INSUFFICIENT_SCOPE
60
+ end
61
+ end
62
+ end
63
+
64
+ end
65
+ end
66
+
@@ -0,0 +1,168 @@
1
+ module OAuth2
2
+ class Provider
3
+
4
+ class Authorization
5
+ attr_reader :owner, :client,
6
+ :code, :access_token,
7
+ :expires_in, :refresh_token,
8
+ :error, :error_description
9
+
10
+ REQUIRED_PARAMS = [RESPONSE_TYPE, CLIENT_ID, REDIRECT_URI]
11
+ VALID_PARAMS = REQUIRED_PARAMS + [SCOPE, STATE]
12
+ VALID_RESPONSES = [CODE, TOKEN, CODE_AND_TOKEN]
13
+
14
+ def initialize(resource_owner, params)
15
+ @owner = resource_owner
16
+ @params = params
17
+ @scope = params[SCOPE]
18
+ @state = params[STATE]
19
+
20
+ validate!
21
+ return unless @owner and not @error
22
+
23
+ @model = Model::Authorization.for(@owner, @client)
24
+ return unless @model and @model.in_scope?(scopes) and not @model.expired?
25
+
26
+ @authorized = true
27
+ @code = @model.generate_code
28
+ end
29
+
30
+ def scopes
31
+ @scope ? @scope.split(/\s+/).delete_if { |s| s.empty? } : []
32
+ end
33
+
34
+ def unauthorized_scopes
35
+ @model ? scopes.select { |s| not @model.in_scope?(s) } : scopes
36
+ end
37
+
38
+ def grant_access!(options = {})
39
+ @model = Model::Authorization.for_response_type(@params[RESPONSE_TYPE],
40
+ :owner => @owner,
41
+ :client => @client,
42
+ :scope => @scope,
43
+ :duration => options[:duration])
44
+
45
+ @code = @model.code
46
+ @access_token = @model.access_token
47
+ @refresh_token = @model.refresh_token
48
+ @expires_in = @model.expires_in
49
+
50
+ unless @params[RESPONSE_TYPE] == CODE
51
+ @expires_in = @model.expires_in
52
+ end
53
+
54
+ @authorized = true
55
+ end
56
+
57
+ def deny_access!
58
+ @code = @access_token = @refresh_token = nil
59
+ @error = ACCESS_DENIED
60
+ @error_description = "The user denied you access"
61
+ end
62
+
63
+ def params
64
+ params = {}
65
+ VALID_PARAMS.each { |key| params[key] = @params[key] if @params.has_key?(key) }
66
+ params
67
+ end
68
+
69
+ def redirect?
70
+ @client and (@authorized or not valid?)
71
+ end
72
+
73
+ def redirect_uri
74
+ return nil unless @client
75
+ base_redirect_uri = @client.redirect_uri
76
+
77
+ if not valid?
78
+ query = to_query_string(ERROR, ERROR_DESCRIPTION, STATE)
79
+ "#{ base_redirect_uri }?#{ query }"
80
+
81
+ elsif @params[RESPONSE_TYPE] == CODE_AND_TOKEN
82
+ query = to_query_string(CODE, STATE)
83
+ fragment = to_query_string(ACCESS_TOKEN, EXPIRES_IN, SCOPE)
84
+ "#{ base_redirect_uri }#{ query.empty? ? '' : '?' + query }##{ fragment }"
85
+
86
+ elsif @params[RESPONSE_TYPE] == 'token'
87
+ fragment = to_query_string(ACCESS_TOKEN, EXPIRES_IN, SCOPE, STATE)
88
+ "#{ base_redirect_uri }##{ fragment }"
89
+
90
+ else
91
+ query = to_query_string(CODE, SCOPE, STATE)
92
+ "#{ base_redirect_uri }?#{ query }"
93
+ end
94
+ end
95
+
96
+ def response_body
97
+ return nil if @client and valid?
98
+ JSON.unparse(
99
+ ERROR => INVALID_REQUEST,
100
+ ERROR_DESCRIPTION => 'This is not a valid OAuth request')
101
+ end
102
+
103
+ def response_headers
104
+ valid? ? {} : Exchange::RESPONSE_HEADERS
105
+ end
106
+
107
+ def response_status
108
+ return 200 if valid?
109
+ @client ? 302 : 400
110
+ end
111
+
112
+ def valid?
113
+ @error.nil?
114
+ end
115
+
116
+ private
117
+
118
+ def validate!
119
+ @client = @params[CLIENT_ID] && Model::Client.find_by_client_id(@params[CLIENT_ID])
120
+ unless @client
121
+ @error = INVALID_CLIENT
122
+ @error_description = "Unknown client ID #{@params[CLIENT_ID]}"
123
+ end
124
+
125
+ REQUIRED_PARAMS.each do |param|
126
+ next if @params.has_key?(param)
127
+ @error = INVALID_REQUEST
128
+ @error_description = "Missing required parameter #{param}"
129
+ end
130
+ return if @error
131
+
132
+ [SCOPE, STATE].each do |param|
133
+ next unless @params.has_key?(param)
134
+ if @params[param] =~ /\r\n/
135
+ @error = INVALID_REQUEST
136
+ @error_description = "Illegal value for #{param} parameter"
137
+ end
138
+ end
139
+
140
+ unless VALID_RESPONSES.include?(@params[RESPONSE_TYPE])
141
+ @error = UNSUPPORTED_RESPONSE
142
+ @error_description = "Response type #{@params[RESPONSE_TYPE]} is not supported"
143
+ end
144
+
145
+ @client = Model::Client.find_by_client_id(@params[CLIENT_ID])
146
+ unless @client
147
+ @error = INVALID_CLIENT
148
+ @error_description = "Unknown client ID #{@params[CLIENT_ID]}"
149
+ end
150
+
151
+ if @client and @client.redirect_uri and @client.redirect_uri != @params[REDIRECT_URI]
152
+ @error = REDIRECT_MISMATCH
153
+ @error_description = "Parameter redirect_uri does not match registered URI"
154
+ end
155
+ end
156
+
157
+ def to_query_string(*ivars)
158
+ ivars.map { |key|
159
+ value = instance_variable_get("@#{key}")
160
+ value = value.join(' ') if Array === value
161
+ value ? "#{ key }=#{ CGI.escape(value.to_s) }" : nil
162
+ }.compact.join('&')
163
+ end
164
+ end
165
+
166
+ end
167
+ end
168
+