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