songkick-oauth2-provider 0.10.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 (47) hide show
  1. data/README.rdoc +394 -0
  2. data/example/README.rdoc +11 -0
  3. data/example/application.rb +159 -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/error.erb +6 -0
  14. data/example/views/home.erb +25 -0
  15. data/example/views/layout.erb +25 -0
  16. data/example/views/login.erb +20 -0
  17. data/example/views/new_client.erb +25 -0
  18. data/example/views/new_user.erb +22 -0
  19. data/example/views/show_client.erb +15 -0
  20. data/lib/songkick/oauth2/model.rb +20 -0
  21. data/lib/songkick/oauth2/model/authorization.rb +126 -0
  22. data/lib/songkick/oauth2/model/client.rb +61 -0
  23. data/lib/songkick/oauth2/model/client_owner.rb +15 -0
  24. data/lib/songkick/oauth2/model/hashing.rb +29 -0
  25. data/lib/songkick/oauth2/model/resource_owner.rb +54 -0
  26. data/lib/songkick/oauth2/provider.rb +122 -0
  27. data/lib/songkick/oauth2/provider/access_token.rb +68 -0
  28. data/lib/songkick/oauth2/provider/authorization.rb +190 -0
  29. data/lib/songkick/oauth2/provider/error.rb +22 -0
  30. data/lib/songkick/oauth2/provider/exchange.rb +227 -0
  31. data/lib/songkick/oauth2/router.rb +79 -0
  32. data/lib/songkick/oauth2/schema.rb +17 -0
  33. data/lib/songkick/oauth2/schema/20120828112156_songkick_oauth2_schema_original_schema.rb +36 -0
  34. data/spec/factories.rb +27 -0
  35. data/spec/request_helpers.rb +52 -0
  36. data/spec/songkick/oauth2/model/authorization_spec.rb +216 -0
  37. data/spec/songkick/oauth2/model/client_spec.rb +55 -0
  38. data/spec/songkick/oauth2/model/resource_owner_spec.rb +88 -0
  39. data/spec/songkick/oauth2/provider/access_token_spec.rb +125 -0
  40. data/spec/songkick/oauth2/provider/authorization_spec.rb +346 -0
  41. data/spec/songkick/oauth2/provider/exchange_spec.rb +353 -0
  42. data/spec/songkick/oauth2/provider_spec.rb +545 -0
  43. data/spec/spec_helper.rb +62 -0
  44. data/spec/test_app/helper.rb +33 -0
  45. data/spec/test_app/provider/application.rb +68 -0
  46. data/spec/test_app/provider/views/authorize.erb +19 -0
  47. metadata +273 -0
@@ -0,0 +1,22 @@
1
+ module Songkick
2
+ module OAuth2
3
+ class Provider
4
+
5
+ class Error
6
+ def initialize(message = nil)
7
+ @message = message
8
+ end
9
+
10
+ def error
11
+ INVALID_REQUEST
12
+ end
13
+
14
+ def error_description
15
+ 'Bad request' + (@message ? ": #{@message}" : '')
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,227 @@
1
+ module Songkick
2
+ module OAuth2
3
+ class Provider
4
+
5
+ class Exchange
6
+ attr_reader :client, :error, :error_description
7
+
8
+ REQUIRED_PARAMS = [CLIENT_ID, CLIENT_SECRET, GRANT_TYPE]
9
+ VALID_GRANT_TYPES = [AUTHORIZATION_CODE, PASSWORD, ASSERTION, REFRESH_TOKEN]
10
+
11
+ REQUIRED_PASSWORD_PARAMS = [USERNAME, PASSWORD]
12
+ REQUIRED_ASSERTION_PARAMS = [ASSERTION_TYPE, ASSERTION]
13
+
14
+ RESPONSE_HEADERS = {
15
+ 'Cache-Control' => 'no-store',
16
+ 'Content-Type' => 'application/json'
17
+ }
18
+
19
+ def initialize(resource_owner, params, transport_error = nil)
20
+ @params = params
21
+ @scope = params[SCOPE]
22
+ @grant_type = @params[GRANT_TYPE]
23
+
24
+ @transport_error = transport_error
25
+
26
+ validate!
27
+ end
28
+
29
+ def owner
30
+ @authorization && @authorization.owner
31
+ end
32
+
33
+ def redirect?
34
+ false
35
+ end
36
+
37
+ def response_body
38
+ return jsonize(ERROR, ERROR_DESCRIPTION) unless valid?
39
+ update_authorization
40
+
41
+ response = {}
42
+ [ACCESS_TOKEN, REFRESH_TOKEN, SCOPE].each do |key|
43
+ value = @authorization.__send__(key)
44
+ response[key] = value if value
45
+ end
46
+ if expiry = @authorization.expires_in
47
+ response[EXPIRES_IN] = expiry
48
+ end
49
+
50
+ JSON.unparse(response)
51
+ end
52
+
53
+ def response_headers
54
+ RESPONSE_HEADERS
55
+ end
56
+
57
+ def response_status
58
+ valid? ? 200 : 400
59
+ end
60
+
61
+ def scopes
62
+ scopes = @scope ? @scope.split(/\s+/).delete_if { |s| s.empty? } : []
63
+ Set.new(scopes)
64
+ end
65
+
66
+ def update_authorization
67
+ return if not valid? or @already_updated
68
+ @authorization.exchange!
69
+ @already_updated = true
70
+ end
71
+
72
+ def valid?
73
+ @error.nil?
74
+ end
75
+
76
+ private
77
+
78
+ def jsonize(*ivars)
79
+ hash = {}
80
+ ivars.each { |key| hash[key] = instance_variable_get("@#{key}") }
81
+ JSON.unparse(hash)
82
+ end
83
+
84
+ def validate!
85
+ if @transport_error
86
+ @error = @transport_error.error
87
+ @error_description = @transport_error.error_description
88
+ return
89
+ end
90
+
91
+ validate_required_params
92
+
93
+ return if @error
94
+ validate_client
95
+
96
+ unless VALID_GRANT_TYPES.include?(@grant_type)
97
+ @error = UNSUPPORTED_GRANT_TYPE
98
+ @error_description = "The grant type #{@grant_type} is not recognized"
99
+ end
100
+ return if @error
101
+
102
+ __send__("validate_#{@grant_type}")
103
+ validate_scope
104
+ end
105
+
106
+ def validate_required_params
107
+ REQUIRED_PARAMS.each do |param|
108
+ next if @params.has_key?(param)
109
+ @error = INVALID_REQUEST
110
+ @error_description = "Missing required parameter #{param}"
111
+ end
112
+ end
113
+
114
+ def validate_client
115
+ @client = Model::Client.find_by_client_id(@params[CLIENT_ID])
116
+ unless @client
117
+ @error = INVALID_CLIENT
118
+ @error_description = "Unknown client ID #{@params[CLIENT_ID]}"
119
+ end
120
+
121
+ if @client and not @client.valid_client_secret?(@params[CLIENT_SECRET])
122
+ @error = INVALID_CLIENT
123
+ @error_description = 'Parameter client_secret does not match'
124
+ end
125
+ end
126
+
127
+ def validate_scope
128
+ if @authorization and not @authorization.in_scope?(scopes)
129
+ @error = INVALID_SCOPE
130
+ @error_description = 'The request scope was never granted by the user'
131
+ end
132
+ end
133
+
134
+ def validate_authorization_code
135
+ unless @params[CODE]
136
+ @error = INVALID_REQUEST
137
+ @error_description = "Missing required parameter code"
138
+ end
139
+
140
+ if @client.redirect_uri and @client.redirect_uri != @params[REDIRECT_URI]
141
+ @error = REDIRECT_MISMATCH
142
+ @error_description = "Parameter redirect_uri does not match registered URI"
143
+ end
144
+
145
+ unless @params.has_key?(REDIRECT_URI)
146
+ @error = INVALID_REQUEST
147
+ @error_description = "Missing required parameter redirect_uri"
148
+ end
149
+
150
+ return if @error
151
+
152
+ @authorization = @client.authorizations.find_by_code(@params[CODE])
153
+ validate_authorization
154
+ end
155
+
156
+ def validate_password
157
+ REQUIRED_PASSWORD_PARAMS.each do |param|
158
+ next if @params.has_key?(param)
159
+ @error = INVALID_REQUEST
160
+ @error_description = "Missing required parameter #{param}"
161
+ end
162
+
163
+ return if @error
164
+
165
+ @authorization = Provider.handle_password(@client, @params[USERNAME], @params[PASSWORD], scopes)
166
+ return validate_authorization if @authorization
167
+
168
+ @error = INVALID_GRANT
169
+ @error_description = 'The access grant you supplied is invalid'
170
+ end
171
+
172
+ def validate_assertion
173
+ REQUIRED_ASSERTION_PARAMS.each do |param|
174
+ next if @params.has_key?(param)
175
+ @error = INVALID_REQUEST
176
+ @error_description = "Missing required parameter #{param}"
177
+ end
178
+
179
+ if @params[ASSERTION_TYPE]
180
+ uri = URI.parse(@params[ASSERTION_TYPE]) rescue nil
181
+ unless uri and uri.absolute?
182
+ @error = INVALID_REQUEST
183
+ @error_description = 'Parameter assertion_type must be an absolute URI'
184
+ end
185
+ end
186
+
187
+ return if @error
188
+
189
+ assertion = Assertion.new(@params)
190
+ @authorization = Provider.handle_assertion(@client, assertion, scopes)
191
+ return validate_authorization if @authorization
192
+
193
+ @error = UNAUTHORIZED_CLIENT
194
+ @error_description = 'Client cannot use the given assertion type'
195
+ end
196
+
197
+ def validate_refresh_token
198
+ refresh_token_hash = Songkick::OAuth2.hashify(@params[REFRESH_TOKEN])
199
+ @authorization = @client.authorizations.find_by_refresh_token_hash(refresh_token_hash)
200
+ validate_authorization
201
+ end
202
+
203
+ def validate_authorization
204
+ unless @authorization
205
+ @error = INVALID_GRANT
206
+ @error_description = 'The access grant you supplied is invalid'
207
+ end
208
+
209
+ if @authorization and @authorization.expired?
210
+ @error = INVALID_GRANT
211
+ @error_description = 'The access grant you supplied is invalid'
212
+ end
213
+ end
214
+ end
215
+
216
+ class Assertion
217
+ attr_reader :type, :value
218
+ def initialize(params)
219
+ @type = params[ASSERTION_TYPE]
220
+ @value = params[ASSERTION]
221
+ end
222
+ end
223
+
224
+ end
225
+ end
226
+ end
227
+
@@ -0,0 +1,79 @@
1
+ require 'base64'
2
+ require 'rack'
3
+
4
+ module Songkick
5
+ module OAuth2
6
+ class Router
7
+
8
+ # Public methods in the namespace take either Rack env objects, or Request
9
+ # objects from Rails/Sinatra and an optional params hash which it then
10
+ # coerces to Rack requests. This is for backward compatibility; originally
11
+ # it only took request objects.
12
+
13
+ class << self
14
+ def parse(resource_owner, env)
15
+ error = detect_transport_error(env)
16
+ request = request_from(env)
17
+ params = request.params
18
+ auth = auth_params(env)
19
+
20
+ if auth[CLIENT_ID] and auth[CLIENT_ID] != params[CLIENT_ID]
21
+ error ||= Provider::Error.new("#{CLIENT_ID} from Basic Auth and request body do not match")
22
+ end
23
+
24
+ params = params.merge(auth)
25
+
26
+ if params[GRANT_TYPE]
27
+ error ||= Provider::Error.new('must be a POST request') unless request.post?
28
+ Provider::Exchange.new(resource_owner, params, error)
29
+ else
30
+ Provider::Authorization.new(resource_owner, params, error)
31
+ end
32
+ end
33
+
34
+ def access_token(resource_owner, scopes, env)
35
+ access_token = access_token_from_request(env)
36
+ Provider::AccessToken.new(resource_owner,
37
+ scopes,
38
+ access_token,
39
+ detect_transport_error(env))
40
+ end
41
+
42
+ def access_token_from_request(env)
43
+ request = request_from(env)
44
+ params = request.params
45
+ header = request.env['HTTP_AUTHORIZATION']
46
+
47
+ header && header =~ /^OAuth\s+/ ?
48
+ header.gsub(/^OAuth\s+/, '') :
49
+ params[OAUTH_TOKEN]
50
+ end
51
+
52
+ private
53
+
54
+ def request_from(env_or_request)
55
+ env = env_or_request.respond_to?(:env) ? env_or_request.env : env_or_request
56
+ env = Rack::MockRequest.env_for(env['REQUEST_URI'] || '', :input => env['RAW_POST_DATA']).merge(env)
57
+ Rack::Request.new(env)
58
+ end
59
+
60
+ def auth_params(env)
61
+ return {} unless basic = env['HTTP_AUTHORIZATION']
62
+ parts = basic.split(/\s+/)
63
+ username, password = Base64.decode64(parts.last).split(':')
64
+ {CLIENT_ID => username, CLIENT_SECRET => password}
65
+ end
66
+
67
+ def detect_transport_error(env)
68
+ request = request_from(env)
69
+
70
+ if Provider.enforce_ssl and not request.ssl?
71
+ Provider::Error.new("must make requests using HTTPS")
72
+ end
73
+ end
74
+ end
75
+
76
+ end
77
+ end
78
+ end
79
+
@@ -0,0 +1,17 @@
1
+ module Songkick
2
+ module OAuth2
3
+
4
+ class Schema
5
+ def self.up
6
+ ActiveRecord::Base.logger ||= Logger.new(StringIO.new)
7
+ ActiveRecord::Migrator.up(migrations_path)
8
+ end
9
+
10
+ def self.migrations_path
11
+ File.expand_path('../schema', __FILE__)
12
+ end
13
+ end
14
+
15
+ end
16
+ end
17
+
@@ -0,0 +1,36 @@
1
+ class SongkickOauth2SchemaOriginalSchema < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :oauth2_clients do |t|
4
+ t.timestamps
5
+ t.string :oauth2_client_owner_type
6
+ t.integer :oauth2_client_owner_id
7
+ t.string :name
8
+ t.string :client_id
9
+ t.string :client_secret_hash
10
+ t.string :redirect_uri
11
+ end
12
+ add_index :oauth2_clients, :client_id
13
+
14
+ create_table :oauth2_authorizations do |t|
15
+ t.timestamps
16
+ t.string :oauth2_resource_owner_type
17
+ t.integer :oauth2_resource_owner_id
18
+ t.belongs_to :client
19
+ t.string :scope
20
+ t.string :code, :limit => 40
21
+ t.string :access_token_hash, :limit => 40
22
+ t.string :refresh_token_hash, :limit => 40
23
+ t.datetime :expires_at
24
+ end
25
+ add_index :oauth2_authorizations, [:client_id, :code]
26
+ add_index :oauth2_authorizations, [:access_token_hash]
27
+ add_index :oauth2_authorizations, [:client_id, :access_token_hash]
28
+ add_index :oauth2_authorizations, [:client_id, :refresh_token_hash]
29
+ end
30
+
31
+ def self.down
32
+ drop_table :oauth2_clients
33
+ drop_table :oauth2_authorizations
34
+ end
35
+ end
36
+
@@ -0,0 +1,27 @@
1
+ require 'factory_girl'
2
+
3
+ Factory.sequence :client_name do |n|
4
+ "Client ##{n}"
5
+ end
6
+
7
+ Factory.sequence :user_name do |n|
8
+ "User ##{n}"
9
+ end
10
+
11
+ Factory.define :owner, :class => TestApp::User do |u|
12
+ u.name { Factory.next :user_name }
13
+ end
14
+
15
+ Factory.define :client, :class => Songkick::OAuth2::Model::Client do |c|
16
+ c.client_id { Songkick::OAuth2.random_string }
17
+ c.client_secret { Songkick::OAuth2.random_string }
18
+ c.name { Factory.next :client_name }
19
+ c.redirect_uri 'https://client.example.com/cb'
20
+ end
21
+
22
+ Factory.define :authorization, :class => Songkick::OAuth2::Model::Authorization do |ac|
23
+ ac.client Factory(:client)
24
+ ac.code { Songkick::OAuth2.random_string }
25
+ ac.expires_at nil
26
+ end
27
+
@@ -0,0 +1,52 @@
1
+ module RequestHelpers
2
+ require 'net/http'
3
+
4
+ def get(query_params)
5
+ qs = params.map { |k,v| "#{ CGI.escape k.to_s }=#{ CGI.escape v.to_s }" }.join('&')
6
+ uri = URI.parse('http://localhost:8000/authorize?' + qs)
7
+ Net::HTTP.get_response(uri)
8
+ end
9
+
10
+ def allow_or_deny(query_params)
11
+ Net::HTTP.post_form(URI.parse('http://localhost:8000/allow'), query_params)
12
+ end
13
+
14
+ def post_basic_auth(auth_params, query_params)
15
+ url = "http://#{ auth_params['client_id'] }:#{ auth_params['client_secret'] }@localhost:8000/authorize"
16
+ Net::HTTP.post_form(URI.parse(url), query_params)
17
+ end
18
+
19
+ def post(query_params)
20
+ Net::HTTP.post_form(URI.parse('http://localhost:8000/authorize'), query_params)
21
+ end
22
+
23
+ def validate_response(response, status, body)
24
+ response.code.to_i.should == status
25
+ response.body.should == body
26
+ response['Cache-Control'].should == 'no-store'
27
+ end
28
+
29
+ def validate_json_response(response, status, body)
30
+ response.code.to_i.should == status
31
+ JSON.parse(response.body).should == body
32
+ response['Content-Type'].should == 'application/json'
33
+ response['Cache-Control'].should == 'no-store'
34
+ end
35
+
36
+ def mock_request(request_class, stubs = {})
37
+ mock_request = mock(request_class)
38
+ method_stubs = {
39
+ :redirect? => false,
40
+ :response_body => nil,
41
+ :response_headers => {},
42
+ :response_status => 200
43
+ }.merge(stubs)
44
+
45
+ method_stubs.each do |method, value|
46
+ mock_request.should_receive(method).and_return(value)
47
+ end
48
+
49
+ mock_request
50
+ end
51
+ end
52
+