songkick-oauth2-provider 0.10.2 → 0.10.3

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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/History.txt +7 -0
  3. data/README.rdoc +18 -11
  4. data/example/README.rdoc +1 -1
  5. data/example/application.rb +9 -9
  6. data/example/schema.rb +1 -1
  7. data/example/views/authorize.erb +2 -2
  8. data/example/views/layout.erb +4 -4
  9. data/example/views/login.erb +2 -2
  10. data/example/views/new_client.erb +1 -1
  11. data/example/views/new_user.erb +1 -1
  12. data/lib/songkick/oauth2/model.rb +8 -6
  13. data/lib/songkick/oauth2/model/authorization.rb +31 -31
  14. data/lib/songkick/oauth2/model/client.rb +15 -15
  15. data/lib/songkick/oauth2/model/client_owner.rb +2 -2
  16. data/lib/songkick/oauth2/model/hashing.rb +3 -3
  17. data/lib/songkick/oauth2/model/helpers.rb +16 -0
  18. data/lib/songkick/oauth2/model/resource_owner.rb +4 -4
  19. data/lib/songkick/oauth2/provider.rb +16 -16
  20. data/lib/songkick/oauth2/provider/access_token.rb +20 -15
  21. data/lib/songkick/oauth2/provider/authorization.rb +43 -42
  22. data/lib/songkick/oauth2/provider/error.rb +4 -4
  23. data/lib/songkick/oauth2/provider/exchange.rb +46 -46
  24. data/lib/songkick/oauth2/router.rb +13 -13
  25. data/lib/songkick/oauth2/schema.rb +11 -3
  26. data/lib/songkick/oauth2/schema/20120828112156_songkick_oauth2_schema_original_schema.rb +2 -2
  27. data/lib/songkick/oauth2/schema/20121024180930_songkick_oauth2_schema_add_authorization_index.rb +3 -3
  28. data/lib/songkick/oauth2/schema/20121025180447_songkick_oauth2_schema_add_unique_indexes.rb +7 -7
  29. data/spec/request_helpers.rb +25 -21
  30. data/spec/songkick/oauth2/model/authorization_spec.rb +56 -56
  31. data/spec/songkick/oauth2/model/client_spec.rb +9 -9
  32. data/spec/songkick/oauth2/model/helpers_spec.rb +26 -0
  33. data/spec/songkick/oauth2/model/resource_owner_spec.rb +13 -13
  34. data/spec/songkick/oauth2/provider/access_token_spec.rb +32 -20
  35. data/spec/songkick/oauth2/provider/authorization_spec.rb +73 -62
  36. data/spec/songkick/oauth2/provider/exchange_spec.rb +72 -72
  37. data/spec/songkick/oauth2/provider_spec.rb +101 -101
  38. data/spec/spec_helper.rb +5 -3
  39. data/spec/test_app/helper.rb +11 -7
  40. data/spec/test_app/provider/application.rb +12 -12
  41. data/spec/test_app/provider/views/authorize.erb +2 -2
  42. metadata +71 -93
@@ -1,58 +1,58 @@
1
1
  module Songkick
2
2
  module OAuth2
3
3
  module Model
4
-
4
+
5
5
  class Client < ActiveRecord::Base
6
6
  self.table_name = :oauth2_clients
7
-
7
+
8
8
  belongs_to :oauth2_client_owner, :polymorphic => true
9
9
  alias :owner :oauth2_client_owner
10
10
  alias :owner= :oauth2_client_owner=
11
-
11
+
12
12
  has_many :authorizations, :class_name => 'Songkick::OAuth2::Model::Authorization', :dependent => :destroy
13
-
13
+
14
14
  validates_uniqueness_of :client_id, :name
15
15
  validates_presence_of :name, :redirect_uri
16
16
  validate :check_format_of_redirect_uri
17
-
17
+
18
18
  attr_accessible :name, :redirect_uri
19
-
19
+
20
20
  before_create :generate_credentials
21
-
21
+
22
22
  def self.create_client_id
23
23
  Songkick::OAuth2.generate_id do |client_id|
24
- count(:conditions => {:client_id => client_id}).zero?
24
+ Helpers.count(self, :client_id => client_id).zero?
25
25
  end
26
26
  end
27
-
27
+
28
28
  attr_reader :client_secret
29
-
29
+
30
30
  def client_secret=(secret)
31
31
  @client_secret = secret
32
32
  hash = BCrypt::Password.create(secret)
33
33
  hash.force_encoding('UTF-8') if hash.respond_to?(:force_encoding)
34
34
  self.client_secret_hash = hash
35
35
  end
36
-
36
+
37
37
  def valid_client_secret?(secret)
38
38
  BCrypt::Password.new(client_secret_hash) == secret
39
39
  end
40
-
40
+
41
41
  private
42
-
42
+
43
43
  def check_format_of_redirect_uri
44
44
  uri = URI.parse(redirect_uri)
45
45
  errors.add(:redirect_uri, 'must be an absolute URI') unless uri.absolute?
46
46
  rescue
47
47
  errors.add(:redirect_uri, 'must be a URI')
48
48
  end
49
-
49
+
50
50
  def generate_credentials
51
51
  self.client_id = self.class.create_client_id
52
52
  self.client_secret = Songkick::OAuth2.random_string
53
53
  end
54
54
  end
55
-
55
+
56
56
  end
57
57
  end
58
58
  end
@@ -1,7 +1,7 @@
1
1
  module Songkick
2
2
  module OAuth2
3
3
  module Model
4
-
4
+
5
5
  module ClientOwner
6
6
  def self.included(klass)
7
7
  klass.has_many :oauth2_clients,
@@ -9,7 +9,7 @@ module Songkick
9
9
  :as => :oauth2_client_owner
10
10
  end
11
11
  end
12
-
12
+
13
13
  end
14
14
  end
15
15
  end
@@ -1,7 +1,7 @@
1
1
  module Songkick
2
2
  module OAuth2
3
3
  module Model
4
-
4
+
5
5
  module Hashing
6
6
  def hashes_attributes(*attributes)
7
7
  attributes.each do |attribute|
@@ -11,7 +11,7 @@ module Songkick
11
11
  end
12
12
  attr_reader attribute
13
13
  end
14
-
14
+
15
15
  class_eval <<-RUBY
16
16
  def reload(*args)
17
17
  super
@@ -22,7 +22,7 @@ module Songkick
22
22
  RUBY
23
23
  end
24
24
  end
25
-
25
+
26
26
  end
27
27
  end
28
28
  end
@@ -0,0 +1,16 @@
1
+ module Songkick
2
+ module OAuth2
3
+ module Model
4
+
5
+ module Helpers
6
+ def self.count(model, conditions={})
7
+ if model.respond_to?(:where)
8
+ model.where(conditions).count
9
+ else
10
+ model.count(:conditions => conditions)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,7 +1,7 @@
1
1
  module Songkick
2
2
  module OAuth2
3
3
  module Model
4
-
4
+
5
5
  module ResourceOwner
6
6
  def self.included(klass)
7
7
  klass.has_many :oauth2_authorizations,
@@ -9,16 +9,16 @@ module Songkick
9
9
  :as => :oauth2_resource_owner,
10
10
  :dependent => :destroy
11
11
  end
12
-
12
+
13
13
  def grant_access!(client, options = {})
14
14
  Authorization.for(self, client, options)
15
15
  end
16
-
16
+
17
17
  def oauth2_authorization_for(client)
18
18
  oauth2_authorizations.find_by_client_id(client.id)
19
19
  end
20
20
  end
21
-
21
+
22
22
  end
23
23
  end
24
24
  end
@@ -15,11 +15,11 @@ module Songkick
15
15
  module OAuth2
16
16
  ROOT = File.expand_path(File.dirname(__FILE__) + '/..')
17
17
  TOKEN_SIZE = 160
18
-
18
+
19
19
  autoload :Model, ROOT + '/oauth2/model'
20
20
  autoload :Router, ROOT + '/oauth2/router'
21
21
  autoload :Schema, ROOT + '/oauth2/schema'
22
-
22
+
23
23
  def self.random_string
24
24
  if defined? SecureRandom
25
25
  SecureRandom.hex(TOKEN_SIZE / 8).to_i(16).to_s(36)
@@ -27,18 +27,18 @@ module Songkick
27
27
  rand(2 ** TOKEN_SIZE).to_s(36)
28
28
  end
29
29
  end
30
-
30
+
31
31
  def self.generate_id(&predicate)
32
32
  id = random_string
33
33
  id = random_string until predicate.call(id)
34
34
  id
35
35
  end
36
-
36
+
37
37
  def self.hashify(token)
38
38
  return nil unless String === token
39
39
  Digest::SHA1.hexdigest(token)
40
40
  end
41
-
41
+
42
42
  ACCESS_TOKEN = 'access_token'
43
43
  ASSERTION = 'assertion'
44
44
  ASSERTION_TYPE = 'assertion_type'
@@ -61,7 +61,7 @@ module Songkick
61
61
  STATE = 'state'
62
62
  TOKEN = 'token'
63
63
  USERNAME = 'username'
64
-
64
+
65
65
  INVALID_REQUEST = 'invalid_request'
66
66
  UNSUPPORTED_RESPONSE = 'unsupported_response_type'
67
67
  REDIRECT_MISMATCH = 'redirect_uri_mismatch'
@@ -74,7 +74,7 @@ module Songkick
74
74
  EXPIRED_TOKEN = 'expired_token'
75
75
  INSUFFICIENT_SCOPE = 'insufficient_scope'
76
76
  ACCESS_DENIED = 'access_denied'
77
-
77
+
78
78
  class Provider
79
79
  EXPIRY_TIME = 3600
80
80
 
@@ -86,42 +86,42 @@ module Songkick
86
86
  class << self
87
87
  attr_accessor :realm, :enforce_ssl
88
88
  end
89
-
89
+
90
90
  def self.clear_assertion_handlers!
91
91
  @password_handler = nil
92
92
  @assertion_handlers = {}
93
93
  @assertion_filters = []
94
94
  end
95
-
95
+
96
96
  clear_assertion_handlers!
97
-
97
+
98
98
  def self.handle_passwords(&block)
99
99
  @password_handler = block
100
100
  end
101
-
101
+
102
102
  def self.handle_password(client, username, password, scopes)
103
103
  return nil unless @password_handler
104
104
  @password_handler.call(client, username, password, scopes)
105
105
  end
106
-
106
+
107
107
  def self.filter_assertions(&filter)
108
108
  @assertion_filters.push(filter)
109
109
  end
110
-
110
+
111
111
  def self.handle_assertions(assertion_type, &handler)
112
112
  @assertion_handlers[assertion_type] = handler
113
113
  end
114
-
114
+
115
115
  def self.handle_assertion(client, assertion, scopes)
116
116
  return nil unless @assertion_filters.all? { |f| f.call(client) }
117
117
  handler = @assertion_handlers[assertion.type]
118
118
  handler ? handler.call(client, assertion.value, scopes) : nil
119
119
  end
120
-
120
+
121
121
  def self.parse(*args)
122
122
  Router.parse(*args)
123
123
  end
124
-
124
+
125
125
  def self.access_token(*args)
126
126
  Router.access_token(*args)
127
127
  end
@@ -1,35 +1,35 @@
1
1
  module Songkick
2
2
  module OAuth2
3
3
  class Provider
4
-
4
+
5
5
  class AccessToken
6
6
  attr_reader :authorization
7
-
7
+
8
8
  def initialize(resource_owner = nil, scopes = [], access_token = nil, error = nil)
9
9
  @resource_owner = resource_owner
10
10
  @scopes = scopes
11
11
  @access_token = access_token
12
12
  @error = error && INVALID_REQUEST
13
-
13
+
14
14
  authorize!(access_token, error)
15
15
  validate!
16
16
  end
17
-
17
+
18
18
  def client
19
19
  valid? ? @authorization.client : nil
20
20
  end
21
-
21
+
22
22
  def owner
23
23
  valid? ? @authorization.owner : nil
24
24
  end
25
-
25
+
26
26
  def response_headers
27
27
  return {} if valid?
28
28
  error_message = "OAuth realm='#{ Provider.realm }'"
29
29
  error_message << ", error='#{ @error }'" unless @error == ''
30
30
  {'WWW-Authenticate' => error_message}
31
31
  end
32
-
32
+
33
33
  def response_status
34
34
  case @error
35
35
  when INVALID_REQUEST, INVALID_TOKEN, EXPIRED_TOKEN then 401
@@ -38,30 +38,35 @@ module Songkick
38
38
  else 200
39
39
  end
40
40
  end
41
-
41
+
42
42
  def valid?
43
43
  @error.nil?
44
44
  end
45
-
45
+
46
46
  private
47
-
47
+
48
48
  def authorize!(access_token, error)
49
49
  return unless @authorization = Model.find_access_token(access_token)
50
50
  @authorization.update_attribute(:access_token, nil) if error
51
51
  end
52
-
52
+
53
53
  def validate!
54
54
  return @error = '' unless @access_token
55
55
  return @error = INVALID_TOKEN unless @authorization
56
56
  return @error = EXPIRED_TOKEN if @authorization.expired?
57
57
  return @error = INSUFFICIENT_SCOPE unless @authorization.in_scope?(@scopes)
58
-
59
- if @resource_owner and @authorization.owner != @resource_owner
60
- @error = INSUFFICIENT_SCOPE
58
+
59
+ case @resource_owner
60
+ when :implicit
61
+ # no error
62
+ when nil
63
+ @error = INVALID_TOKEN
64
+ else
65
+ @error = INSUFFICIENT_SCOPE if @authorization.owner != @resource_owner
61
66
  end
62
67
  end
63
68
  end
64
-
69
+
65
70
  end
66
71
  end
67
72
  end
@@ -1,152 +1,153 @@
1
1
  module Songkick
2
2
  module OAuth2
3
3
  class Provider
4
-
4
+
5
5
  class Authorization
6
6
  attr_reader :owner, :client,
7
7
  :code, :access_token,
8
8
  :expires_in, :refresh_token,
9
9
  :error, :error_description
10
-
10
+
11
11
  REQUIRED_PARAMS = [RESPONSE_TYPE, CLIENT_ID, REDIRECT_URI]
12
12
  VALID_PARAMS = REQUIRED_PARAMS + [SCOPE, STATE]
13
13
  VALID_RESPONSES = [CODE, TOKEN, CODE_AND_TOKEN]
14
-
14
+
15
15
  def initialize(resource_owner, params, transport_error = nil)
16
16
  @owner = resource_owner
17
17
  @params = params
18
18
  @scope = params[SCOPE]
19
19
  @state = params[STATE]
20
-
20
+
21
21
  @transport_error = transport_error
22
-
22
+
23
23
  validate!
24
-
24
+
25
25
  return unless @owner and not @error
26
-
26
+
27
27
  @model = @owner.oauth2_authorization_for(@client)
28
28
  return unless @model and @model.in_scope?(scopes) and not @model.expired?
29
-
29
+
30
30
  @authorized = true
31
-
31
+
32
32
  if @params[RESPONSE_TYPE] =~ /code/
33
33
  @code = @model.generate_code
34
34
  end
35
-
35
+
36
36
  if @params[RESPONSE_TYPE] =~ /token/
37
37
  @access_token = @model.generate_access_token
38
38
  end
39
39
  end
40
-
40
+
41
41
  def scopes
42
42
  scopes = @scope ? @scope.split(/\s+/).delete_if { |s| s.empty? } : []
43
43
  Set.new(scopes)
44
44
  end
45
-
45
+
46
46
  def unauthorized_scopes
47
47
  @model ? scopes.select { |s| not @model.in_scope?(s) } : scopes
48
48
  end
49
-
49
+
50
50
  def grant_access!(options = {})
51
51
  @model = Model::Authorization.for(@owner, @client,
52
52
  :response_type => @params[RESPONSE_TYPE],
53
53
  :scope => @scope,
54
54
  :duration => options[:duration])
55
-
55
+
56
56
  @code = @model.code
57
57
  @access_token = @model.access_token
58
58
  @refresh_token = @model.refresh_token
59
59
  @expires_in = @model.expires_in
60
-
60
+
61
61
  unless @params[RESPONSE_TYPE] == CODE
62
62
  @expires_in = @model.expires_in
63
63
  end
64
-
64
+
65
65
  @authorized = true
66
66
  end
67
-
67
+
68
68
  def deny_access!
69
69
  @code = @access_token = @refresh_token = nil
70
70
  @error = ACCESS_DENIED
71
71
  @error_description = "The user denied you access"
72
72
  end
73
-
73
+
74
74
  def params
75
75
  params = {}
76
76
  VALID_PARAMS.each { |key| params[key] = @params[key] if @params.has_key?(key) }
77
77
  params
78
78
  end
79
-
79
+
80
80
  def redirect?
81
81
  @client and (@authorized or not valid?)
82
82
  end
83
-
83
+
84
84
  def redirect_uri
85
85
  return nil unless @client
86
86
  base_redirect_uri = @client.redirect_uri
87
-
87
+ q = (base_redirect_uri =~ /\?/) ? '&' : '?'
88
+
88
89
  if not valid?
89
90
  query = to_query_string(ERROR, ERROR_DESCRIPTION, STATE)
90
- "#{ base_redirect_uri }?#{ query }"
91
-
91
+ "#{ base_redirect_uri }#{ q }#{ query }"
92
+
92
93
  elsif @params[RESPONSE_TYPE] == CODE_AND_TOKEN
93
94
  query = to_query_string(CODE, STATE)
94
95
  fragment = to_query_string(ACCESS_TOKEN, EXPIRES_IN, SCOPE)
95
- "#{ base_redirect_uri }#{ query.empty? ? '' : '?' + query }##{ fragment }"
96
-
97
- elsif @params[RESPONSE_TYPE] == 'token'
96
+ "#{ base_redirect_uri }#{ query.empty? ? '' : q + query }##{ fragment }"
97
+
98
+ elsif @params[RESPONSE_TYPE] == TOKEN
98
99
  fragment = to_query_string(ACCESS_TOKEN, EXPIRES_IN, SCOPE, STATE)
99
100
  "#{ base_redirect_uri }##{ fragment }"
100
-
101
+
101
102
  else
102
103
  query = to_query_string(CODE, SCOPE, STATE)
103
- "#{ base_redirect_uri }?#{ query }"
104
+ "#{ base_redirect_uri }#{ q }#{ query }"
104
105
  end
105
106
  end
106
-
107
+
107
108
  def response_body
108
109
  warn "Songkick::OAuth2::Provider::Authorization no longer returns a response body "+
109
110
  "when the request is invalid. You should call valid? to determine "+
110
111
  "whether to render your login page or an error page."
111
112
  nil
112
113
  end
113
-
114
+
114
115
  def response_headers
115
116
  redirect? ? {} : {'Cache-Control' => 'no-store'}
116
117
  end
117
-
118
+
118
119
  def response_status
119
120
  return 302 if redirect?
120
121
  return 200 if valid?
121
122
  @client ? 302 : 400
122
123
  end
123
-
124
+
124
125
  def valid?
125
126
  @error.nil?
126
127
  end
127
-
128
+
128
129
  private
129
-
130
+
130
131
  def validate!
131
132
  if @transport_error
132
133
  @error = @transport_error.error
133
134
  @error_description = @transport_error.error_description
134
135
  return
135
136
  end
136
-
137
+
137
138
  @client = @params[CLIENT_ID] && Model::Client.find_by_client_id(@params[CLIENT_ID])
138
139
  unless @client
139
140
  @error = INVALID_CLIENT
140
141
  @error_description = "Unknown client ID #{@params[CLIENT_ID]}"
141
142
  end
142
-
143
+
143
144
  REQUIRED_PARAMS.each do |param|
144
145
  next if @params.has_key?(param)
145
146
  @error = INVALID_REQUEST
146
147
  @error_description = "Missing required parameter #{param}"
147
148
  end
148
149
  return if @error
149
-
150
+
150
151
  [SCOPE, STATE].each do |param|
151
152
  next unless @params.has_key?(param)
152
153
  if @params[param] =~ /\r\n/
@@ -154,24 +155,24 @@ module Songkick
154
155
  @error_description = "Illegal value for #{param} parameter"
155
156
  end
156
157
  end
157
-
158
+
158
159
  unless VALID_RESPONSES.include?(@params[RESPONSE_TYPE])
159
160
  @error = UNSUPPORTED_RESPONSE
160
161
  @error_description = "Response type #{@params[RESPONSE_TYPE]} is not supported"
161
162
  end
162
-
163
+
163
164
  @client = Model::Client.find_by_client_id(@params[CLIENT_ID])
164
165
  unless @client
165
166
  @error = INVALID_CLIENT
166
167
  @error_description = "Unknown client ID #{@params[CLIENT_ID]}"
167
168
  end
168
-
169
+
169
170
  if @client and @client.redirect_uri and @client.redirect_uri != @params[REDIRECT_URI]
170
171
  @error = REDIRECT_MISMATCH
171
172
  @error_description = "Parameter #{REDIRECT_URI} does not match registered URI"
172
173
  end
173
174
  end
174
-
175
+
175
176
  def to_query_string(*ivars)
176
177
  ivars.map { |key|
177
178
  value = instance_variable_get("@#{key}")
@@ -180,7 +181,7 @@ module Songkick
180
181
  }.compact.join('&')
181
182
  end
182
183
  end
183
-
184
+
184
185
  end
185
186
  end
186
187
  end