songkick-oauth2-provider 0.10.2 → 0.10.3

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