oauth2-provider-jonrowe 0.1.0

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