rockoauth 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.
- checksums.yaml +7 -0
- data/History.txt +5 -0
- data/README.rdoc +422 -0
- data/example/README.rdoc +11 -0
- data/example/application.rb +158 -0
- data/example/config.ru +3 -0
- data/example/environment.rb +11 -0
- data/example/models/connection.rb +9 -0
- data/example/models/note.rb +4 -0
- data/example/models/user.rb +5 -0
- data/example/public/style.css +78 -0
- data/example/schema.rb +22 -0
- data/example/views/authorize.erb +28 -0
- data/example/views/create_user.erb +3 -0
- data/example/views/error.erb +6 -0
- data/example/views/home.erb +24 -0
- data/example/views/layout.erb +24 -0
- data/example/views/login.erb +20 -0
- data/example/views/new_client.erb +25 -0
- data/example/views/new_user.erb +22 -0
- data/example/views/show_client.erb +15 -0
- data/lib/rockoauth/model/authorization.rb +132 -0
- data/lib/rockoauth/model/client.rb +54 -0
- data/lib/rockoauth/model/client_owner.rb +13 -0
- data/lib/rockoauth/model/hashing.rb +26 -0
- data/lib/rockoauth/model/helpers.rb +14 -0
- data/lib/rockoauth/model/resource_owner.rb +22 -0
- data/lib/rockoauth/model.rb +38 -0
- data/lib/rockoauth/provider/access_token.rb +70 -0
- data/lib/rockoauth/provider/authorization.rb +185 -0
- data/lib/rockoauth/provider/error.rb +19 -0
- data/lib/rockoauth/provider/exchange.rb +225 -0
- data/lib/rockoauth/provider.rb +133 -0
- data/lib/rockoauth/router.rb +75 -0
- data/lib/rockoauth/schema/20120828112156_rockoauth_schema_original_schema.rb +35 -0
- data/lib/rockoauth/schema/20121024180930_rockoauth_schema_add_authorization_index.rb +13 -0
- data/lib/rockoauth/schema/20121025180447_rockoauth_schema_add_unique_indexes.rb +31 -0
- data/lib/rockoauth/schema.rb +25 -0
- data/lib/rockoauth.rb +1 -0
- data/spec/factories.rb +20 -0
- data/spec/request_helpers.rb +62 -0
- data/spec/rockoauth/model/authorization_spec.rb +237 -0
- data/spec/rockoauth/model/client_spec.rb +44 -0
- data/spec/rockoauth/model/helpers_spec.rb +25 -0
- data/spec/rockoauth/model/resource_owner_spec.rb +87 -0
- data/spec/rockoauth/provider/access_token_spec.rb +138 -0
- data/spec/rockoauth/provider/authorization_spec.rb +356 -0
- data/spec/rockoauth/provider/exchange_spec.rb +361 -0
- data/spec/rockoauth/provider_spec.rb +560 -0
- data/spec/spec_helper.rb +80 -0
- data/spec/test_app/helper.rb +36 -0
- data/spec/test_app/provider/application.rb +67 -0
- data/spec/test_app/provider/views/authorize.erb +19 -0
- metadata +238 -0
@@ -0,0 +1,185 @@
|
|
1
|
+
module RockOAuth
|
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, transport_error = nil)
|
15
|
+
@owner = resource_owner
|
16
|
+
@params = params
|
17
|
+
@scope = params[SCOPE]
|
18
|
+
@state = params[STATE]
|
19
|
+
|
20
|
+
@transport_error = transport_error
|
21
|
+
|
22
|
+
validate!
|
23
|
+
|
24
|
+
return unless @owner and not @error
|
25
|
+
|
26
|
+
@model = @owner.oauth2_authorization_for(@client)
|
27
|
+
return unless @model and @model.in_scope?(scopes) and not @model.expired?
|
28
|
+
|
29
|
+
@authorized = true
|
30
|
+
|
31
|
+
if @params[RESPONSE_TYPE] =~ /code/
|
32
|
+
@code = @model.generate_code
|
33
|
+
end
|
34
|
+
|
35
|
+
if @params[RESPONSE_TYPE] =~ /token/
|
36
|
+
@access_token = @model.generate_access_token
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def scopes
|
41
|
+
scopes = @scope ? @scope.split(/\s+/).delete_if { |s| s.empty? } : []
|
42
|
+
Set.new(scopes)
|
43
|
+
end
|
44
|
+
|
45
|
+
def unauthorized_scopes
|
46
|
+
@model ? scopes.select { |s| not @model.in_scope?(s) } : scopes
|
47
|
+
end
|
48
|
+
|
49
|
+
def grant_access!(options = {})
|
50
|
+
@model = Model::Authorization.for(@owner, @client,
|
51
|
+
:response_type => @params[RESPONSE_TYPE],
|
52
|
+
:scope => @scope,
|
53
|
+
:duration => options[:duration])
|
54
|
+
|
55
|
+
@code = @model.code
|
56
|
+
@access_token = @model.access_token
|
57
|
+
@refresh_token = @model.refresh_token
|
58
|
+
@expires_in = @model.expires_in
|
59
|
+
|
60
|
+
unless @params[RESPONSE_TYPE] == CODE
|
61
|
+
@expires_in = @model.expires_in
|
62
|
+
end
|
63
|
+
|
64
|
+
@authorized = true
|
65
|
+
end
|
66
|
+
|
67
|
+
def deny_access!
|
68
|
+
@code = @access_token = @refresh_token = nil
|
69
|
+
@error = ACCESS_DENIED
|
70
|
+
@error_description = "The user denied you access"
|
71
|
+
end
|
72
|
+
|
73
|
+
def params
|
74
|
+
params = {}
|
75
|
+
VALID_PARAMS.each { |key| params[key] = @params[key] if @params.has_key?(key) }
|
76
|
+
params
|
77
|
+
end
|
78
|
+
|
79
|
+
def redirect?
|
80
|
+
@client and (@authorized or not valid?)
|
81
|
+
end
|
82
|
+
|
83
|
+
def redirect_uri
|
84
|
+
return nil unless @client
|
85
|
+
base_redirect_uri = @client.redirect_uri
|
86
|
+
q = (base_redirect_uri =~ /\?/) ? '&' : '?'
|
87
|
+
|
88
|
+
if not valid?
|
89
|
+
query = to_query_string(ERROR, ERROR_DESCRIPTION, STATE)
|
90
|
+
"#{ base_redirect_uri }#{ q }#{ query }"
|
91
|
+
|
92
|
+
elsif @params[RESPONSE_TYPE] == CODE_AND_TOKEN
|
93
|
+
query = to_query_string(CODE, STATE)
|
94
|
+
fragment = to_query_string(ACCESS_TOKEN, EXPIRES_IN, SCOPE)
|
95
|
+
"#{ base_redirect_uri }#{ query.empty? ? '' : q + query }##{ fragment }"
|
96
|
+
|
97
|
+
elsif @params[RESPONSE_TYPE] == TOKEN
|
98
|
+
fragment = to_query_string(ACCESS_TOKEN, EXPIRES_IN, SCOPE, STATE)
|
99
|
+
"#{ base_redirect_uri }##{ fragment }"
|
100
|
+
|
101
|
+
else
|
102
|
+
query = to_query_string(CODE, SCOPE, STATE)
|
103
|
+
"#{ base_redirect_uri }#{ q }#{ query }"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def response_body
|
108
|
+
warn "RockOAuth::Provider::Authorization no longer returns a response body "+
|
109
|
+
"when the request is invalid. You should call valid? to determine "+
|
110
|
+
"whether to render your login page or an error page."
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def response_headers
|
115
|
+
redirect? ? {} : {'Cache-Control' => 'no-store'}
|
116
|
+
end
|
117
|
+
|
118
|
+
def response_status
|
119
|
+
return 302 if redirect?
|
120
|
+
return 200 if valid?
|
121
|
+
@client ? 302 : 400
|
122
|
+
end
|
123
|
+
|
124
|
+
def valid?
|
125
|
+
@error.nil?
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def validate!
|
131
|
+
if @transport_error
|
132
|
+
@error = @transport_error.error
|
133
|
+
@error_description = @transport_error.error_description
|
134
|
+
return
|
135
|
+
end
|
136
|
+
|
137
|
+
@client = @params[CLIENT_ID] && Model::Client.find_by_client_id(@params[CLIENT_ID])
|
138
|
+
unless @client
|
139
|
+
@error = INVALID_CLIENT
|
140
|
+
@error_description = "Unknown client ID #{@params[CLIENT_ID]}"
|
141
|
+
end
|
142
|
+
|
143
|
+
REQUIRED_PARAMS.each do |param|
|
144
|
+
next if @params.has_key?(param)
|
145
|
+
@error = INVALID_REQUEST
|
146
|
+
@error_description = "Missing required parameter #{param}"
|
147
|
+
end
|
148
|
+
return if @error
|
149
|
+
|
150
|
+
[SCOPE, STATE].each do |param|
|
151
|
+
next unless @params.has_key?(param)
|
152
|
+
if @params[param] =~ /\r\n/
|
153
|
+
@error = INVALID_REQUEST
|
154
|
+
@error_description = "Illegal value for #{param} parameter"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
unless VALID_RESPONSES.include?(@params[RESPONSE_TYPE])
|
159
|
+
@error = UNSUPPORTED_RESPONSE
|
160
|
+
@error_description = "Response type #{@params[RESPONSE_TYPE]} is not supported"
|
161
|
+
end
|
162
|
+
|
163
|
+
@client = Model::Client.find_by_client_id(@params[CLIENT_ID])
|
164
|
+
unless @client
|
165
|
+
@error = INVALID_CLIENT
|
166
|
+
@error_description = "Unknown client ID #{@params[CLIENT_ID]}"
|
167
|
+
end
|
168
|
+
|
169
|
+
if @client and @client.redirect_uri and @client.redirect_uri != @params[REDIRECT_URI]
|
170
|
+
@error = REDIRECT_MISMATCH
|
171
|
+
@error_description = "Parameter #{REDIRECT_URI} does not match registered URI"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def to_query_string(*ivars)
|
176
|
+
ivars.map { |key|
|
177
|
+
value = instance_variable_get("@#{key}")
|
178
|
+
value = value.join(' ') if Array === value
|
179
|
+
value ? "#{ key }=#{ CGI.escape(value.to_s) }" : nil
|
180
|
+
}.compact.join('&')
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module RockOAuth
|
2
|
+
class Provider
|
3
|
+
|
4
|
+
class Error
|
5
|
+
def initialize(message = nil)
|
6
|
+
@message = message
|
7
|
+
end
|
8
|
+
|
9
|
+
def error
|
10
|
+
INVALID_REQUEST
|
11
|
+
end
|
12
|
+
|
13
|
+
def error_description
|
14
|
+
'Bad request' + (@message ? ": #{@message}" : '')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
module RockOAuth
|
2
|
+
class Provider
|
3
|
+
|
4
|
+
class Exchange
|
5
|
+
attr_reader :client, :error, :error_description
|
6
|
+
|
7
|
+
REQUIRED_PARAMS = [CLIENT_ID, CLIENT_SECRET, GRANT_TYPE]
|
8
|
+
VALID_GRANT_TYPES = [AUTHORIZATION_CODE, PASSWORD, ASSERTION, REFRESH_TOKEN]
|
9
|
+
|
10
|
+
REQUIRED_PASSWORD_PARAMS = [USERNAME, PASSWORD]
|
11
|
+
REQUIRED_ASSERTION_PARAMS = [ASSERTION_TYPE, ASSERTION]
|
12
|
+
|
13
|
+
RESPONSE_HEADERS = {
|
14
|
+
'Cache-Control' => 'no-store',
|
15
|
+
'Content-Type' => 'application/json'
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize(resource_owner, params, transport_error = nil)
|
19
|
+
@params = params
|
20
|
+
@scope = params[SCOPE]
|
21
|
+
@grant_type = @params[GRANT_TYPE]
|
22
|
+
@resource_owner = resource_owner
|
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, @resource_owner)
|
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 = RockOAuth.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
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'bcrypt'
|
3
|
+
require 'cgi'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'json'
|
6
|
+
require 'logger'
|
7
|
+
require 'rack'
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'securerandom'
|
11
|
+
rescue LoadError
|
12
|
+
end
|
13
|
+
|
14
|
+
module RockOAuth
|
15
|
+
ROOT = File.dirname(__FILE__)
|
16
|
+
TOKEN_SIZE = 160
|
17
|
+
|
18
|
+
autoload :Model, ROOT + '/model'
|
19
|
+
autoload :Router, ROOT + '/router'
|
20
|
+
autoload :Schema, ROOT + '/schema'
|
21
|
+
|
22
|
+
def self.random_string
|
23
|
+
if defined? SecureRandom
|
24
|
+
SecureRandom.hex(TOKEN_SIZE / 8).to_i(16).to_s(36)
|
25
|
+
else
|
26
|
+
rand(2 ** TOKEN_SIZE).to_s(36)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.generate_id(&predicate)
|
31
|
+
id = random_string
|
32
|
+
id = random_string until predicate.call(id)
|
33
|
+
id
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.hashify(token)
|
37
|
+
return nil unless String === token
|
38
|
+
Digest::SHA1.hexdigest(token)
|
39
|
+
end
|
40
|
+
|
41
|
+
ACCESS_TOKEN = 'access_token'
|
42
|
+
ASSERTION = 'assertion'
|
43
|
+
ASSERTION_TYPE = 'assertion_type'
|
44
|
+
AUTHORIZATION_CODE = 'authorization_code'
|
45
|
+
CLIENT_ID = 'client_id'
|
46
|
+
CLIENT_SECRET = 'client_secret'
|
47
|
+
CODE = 'code'
|
48
|
+
CODE_AND_TOKEN = 'code_and_token'
|
49
|
+
DURATION = 'duration'
|
50
|
+
ERROR = 'error'
|
51
|
+
ERROR_DESCRIPTION = 'error_description'
|
52
|
+
EXPIRES_IN = 'expires_in'
|
53
|
+
GRANT_TYPE = 'grant_type'
|
54
|
+
OAUTH_TOKEN = 'oauth_token'
|
55
|
+
PASSWORD = 'password'
|
56
|
+
REDIRECT_URI = 'redirect_uri'
|
57
|
+
REFRESH_TOKEN = 'refresh_token'
|
58
|
+
RESPONSE_TYPE = 'response_type'
|
59
|
+
SCOPE = 'scope'
|
60
|
+
STATE = 'state'
|
61
|
+
TOKEN = 'token'
|
62
|
+
USERNAME = 'username'
|
63
|
+
|
64
|
+
INVALID_REQUEST = 'invalid_request'
|
65
|
+
UNSUPPORTED_RESPONSE = 'unsupported_response_type'
|
66
|
+
REDIRECT_MISMATCH = 'redirect_uri_mismatch'
|
67
|
+
UNSUPPORTED_GRANT_TYPE = 'unsupported_grant_type'
|
68
|
+
INVALID_GRANT = 'invalid_grant'
|
69
|
+
INVALID_CLIENT = 'invalid_client'
|
70
|
+
UNAUTHORIZED_CLIENT = 'unauthorized_client'
|
71
|
+
INVALID_SCOPE = 'invalid_scope'
|
72
|
+
INVALID_TOKEN = 'invalid_token'
|
73
|
+
EXPIRED_TOKEN = 'expired_token'
|
74
|
+
INSUFFICIENT_SCOPE = 'insufficient_scope'
|
75
|
+
ACCESS_DENIED = 'access_denied'
|
76
|
+
|
77
|
+
class Provider
|
78
|
+
EXPIRY_TIME = 3600
|
79
|
+
|
80
|
+
autoload :Authorization, ROOT + '/provider/authorization'
|
81
|
+
autoload :Exchange, ROOT + '/provider/exchange'
|
82
|
+
autoload :AccessToken, ROOT + '/provider/access_token'
|
83
|
+
autoload :Error, ROOT + '/provider/error'
|
84
|
+
|
85
|
+
class << self
|
86
|
+
attr_accessor :realm, :enforce_ssl
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.clear_assertion_handlers!
|
90
|
+
@password_handler = nil
|
91
|
+
@assertion_handlers = {}
|
92
|
+
@assertion_filters = []
|
93
|
+
end
|
94
|
+
|
95
|
+
clear_assertion_handlers!
|
96
|
+
|
97
|
+
def self.handle_passwords(&block)
|
98
|
+
@password_handler = block
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.handle_password(client, username, password, scopes)
|
102
|
+
return nil unless @password_handler
|
103
|
+
@password_handler.call(client, username, password, scopes)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.filter_assertions(&filter)
|
107
|
+
@assertion_filters.push(filter)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.handle_assertions(assertion_type, &handler)
|
111
|
+
@assertion_handlers[assertion_type] = handler
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.handle_assertion(client, assertion, scopes, resource_owner)
|
115
|
+
return nil unless @assertion_filters.all? { |f| f.call(client) }
|
116
|
+
handler = @assertion_handlers[assertion.type]
|
117
|
+
handler ? handler.call(client, assertion.value, scopes, resource_owner) : nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.parse(*args)
|
121
|
+
Router.parse(*args)
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.access_token(*args)
|
125
|
+
Router.access_token(*args)
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.access_token_from_request(*args)
|
129
|
+
Router.access_token_from_request(*args)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module RockOAuth
|
2
|
+
class Router
|
3
|
+
|
4
|
+
# Public methods in the namespace take either Rack env objects, or Request
|
5
|
+
# objects from Rails/Sinatra and an optional params hash which it then
|
6
|
+
# coerces to Rack requests. This is for backward compatibility; originally
|
7
|
+
# it only took request objects.
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def parse(resource_owner, env)
|
11
|
+
error = detect_transport_error(env)
|
12
|
+
request = request_from(env)
|
13
|
+
params = request.params
|
14
|
+
auth = auth_params(env)
|
15
|
+
|
16
|
+
if auth[CLIENT_ID] and auth[CLIENT_ID] != params[CLIENT_ID]
|
17
|
+
error ||= Provider::Error.new("#{CLIENT_ID} from Basic Auth and request body do not match")
|
18
|
+
end
|
19
|
+
|
20
|
+
params = params.merge(auth)
|
21
|
+
|
22
|
+
if params[GRANT_TYPE]
|
23
|
+
error ||= Provider::Error.new('must be a POST request') unless request.post?
|
24
|
+
Provider::Exchange.new(resource_owner, params, error)
|
25
|
+
else
|
26
|
+
Provider::Authorization.new(resource_owner, params, error)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def access_token(resource_owner, scopes, env)
|
31
|
+
access_token = access_token_from_request(env)
|
32
|
+
Provider::AccessToken.new(resource_owner,
|
33
|
+
scopes,
|
34
|
+
access_token,
|
35
|
+
detect_transport_error(env))
|
36
|
+
end
|
37
|
+
|
38
|
+
def access_token_from_request(env)
|
39
|
+
request = request_from(env)
|
40
|
+
params = request.params
|
41
|
+
header = request.env['HTTP_AUTHORIZATION']
|
42
|
+
|
43
|
+
header && header =~ /^OAuth\s+/ ?
|
44
|
+
header.gsub(/^OAuth\s+/, '') :
|
45
|
+
params[OAUTH_TOKEN]
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def request_from(env_or_request)
|
51
|
+
env = env_or_request.respond_to?(:env) ? env_or_request.env : env_or_request
|
52
|
+
env = Rack::MockRequest.env_for(env['REQUEST_URI'] || '', :input => env['RAW_POST_DATA']).merge(env)
|
53
|
+
Rack::Request.new(env)
|
54
|
+
end
|
55
|
+
|
56
|
+
def auth_params(env)
|
57
|
+
return {} unless basic = env['HTTP_AUTHORIZATION']
|
58
|
+
parts = basic.split(/\s+/)
|
59
|
+
username, password = Base64.decode64(parts.last).split(':')
|
60
|
+
{CLIENT_ID => username, CLIENT_SECRET => password}
|
61
|
+
end
|
62
|
+
|
63
|
+
def detect_transport_error(env)
|
64
|
+
request = request_from(env)
|
65
|
+
|
66
|
+
if Provider.enforce_ssl and not request.ssl?
|
67
|
+
Provider::Error.new('must make requests using HTTPS')
|
68
|
+
elsif request.GET['client_secret']
|
69
|
+
Provider::Error.new('must not send client credentials in the URI')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class RockoauthSchemaOriginalSchema < 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, [:access_token_hash]
|
26
|
+
add_index :oauth2_authorizations, [:client_id, :code], name: :index_oauth2_client_id_and_code
|
27
|
+
add_index :oauth2_authorizations, [:client_id, :access_token_hash], name: :index_oauth2_client_id_and_access_token_hash
|
28
|
+
add_index :oauth2_authorizations, [:client_id, :refresh_token_hash], name: :index_oauth2_client_id_and_refresh_token_hash
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.down
|
32
|
+
drop_table :oauth2_clients
|
33
|
+
drop_table :oauth2_authorizations
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class RockoauthSchemaAddAuthorizationIndex < ActiveRecord::Migration
|
2
|
+
INDEX_NAME = 'index_owner_client_pairs'
|
3
|
+
|
4
|
+
def self.up
|
5
|
+
remove_index :oauth2_authorizations, name: "index_oauth2_client_id_and_access_token_hash"
|
6
|
+
add_index :oauth2_authorizations, [:client_id, :oauth2_resource_owner_type, :oauth2_resource_owner_id], :name => INDEX_NAME, :unique => true
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.down
|
10
|
+
remove_index :oauth2_authorizations, :name => INDEX_NAME
|
11
|
+
add_index :oauth2_authorizations, [:client_id, :access_token_hash]
|
12
|
+
end
|
13
|
+
end
|