rodauth-oauth 0.0.1
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/CHANGELOG.md +5 -0
- data/README.md +333 -0
- data/lib/generators/roda/oauth/install_generator.rb +47 -0
- data/lib/generators/roda/oauth/templates/app/models/oauth_application.rb +4 -0
- data/lib/generators/roda/oauth/templates/app/models/oauth_grant.rb +4 -0
- data/lib/generators/roda/oauth/templates/app/models/oauth_token.rb +4 -0
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +47 -0
- data/lib/generators/roda/oauth/views_generator.rb +54 -0
- data/lib/rodauth/features/oauth.rb +802 -0
- data/lib/rodauth/oauth.rb +7 -0
- data/lib/rodauth/oauth/railtie.rb +8 -0
- data/lib/rodauth/oauth/version.rb +7 -0
- metadata +59 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
|
5
|
+
module Rodauth::OAuth
|
6
|
+
module Rails
|
7
|
+
module Generators
|
8
|
+
class ViewsGenerator < ::Rails::Generators::Base
|
9
|
+
source_root "#{__dir__}/templates"
|
10
|
+
namespace "roda:oauth:views"
|
11
|
+
|
12
|
+
DEFAULT = %w[oauth_authorize].freeze
|
13
|
+
VIEWS = {
|
14
|
+
oauth_authorize: DEFAULT,
|
15
|
+
oauth_applications: %w[oauth_applications oauth_application new_oauth_application]
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
DEPENDENCIES = {
|
19
|
+
active_sessions: :logout,
|
20
|
+
otp: :two_factor_base,
|
21
|
+
sms_codes: :two_factor_base,
|
22
|
+
recovery_codes: :two_factor_base,
|
23
|
+
webauthn: :two_factor_base
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
class_option :features, type: :array,
|
27
|
+
desc: "Roda OAuth features to generate views for (oauth_applications etc.)",
|
28
|
+
default: DEFAULT
|
29
|
+
|
30
|
+
class_option :all, aliases: "-a", type: :boolean,
|
31
|
+
desc: "Generates views for all Roda OAuth features",
|
32
|
+
default: false
|
33
|
+
|
34
|
+
class_option :directory, aliases: "-d", type: :string,
|
35
|
+
desc: "The directory under app/views/* into which to create views",
|
36
|
+
default: "rodauth"
|
37
|
+
|
38
|
+
def create_views
|
39
|
+
features = options[:all] ? VIEWS.keys : (DEFAULT + options[:features]).map(&:to_sym)
|
40
|
+
|
41
|
+
views = features.inject([]) do |list, feature|
|
42
|
+
list |= VIEWS[feature] || []
|
43
|
+
list |= VIEWS[DEPENDENCIES[feature]] || []
|
44
|
+
end
|
45
|
+
|
46
|
+
views.each do |view|
|
47
|
+
template "app/views/rodauth/#{view}.html.erb",
|
48
|
+
"app/views/#{options[:directory].underscore}/#{view}.html.erb"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,802 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth) do
|
5
|
+
# RUBY EXTENSIONS
|
6
|
+
unless Regexp.method_defined?(:match?)
|
7
|
+
module RegexpExtensions
|
8
|
+
refine(Regexp) do
|
9
|
+
def match?(*args)
|
10
|
+
!match(*args).nil?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
using(RegexpExtensions)
|
15
|
+
end
|
16
|
+
|
17
|
+
SCOPES = %w[profile.read].freeze
|
18
|
+
|
19
|
+
depends :login
|
20
|
+
|
21
|
+
before "authorize"
|
22
|
+
after "authorize"
|
23
|
+
after "authorize_failure"
|
24
|
+
|
25
|
+
before "token"
|
26
|
+
after "token"
|
27
|
+
|
28
|
+
before "revoke"
|
29
|
+
after "revoke"
|
30
|
+
|
31
|
+
before "create_oauth_application"
|
32
|
+
after "create_oauth_application"
|
33
|
+
|
34
|
+
error_flash "OAuth Authorization invalid parameters", "oauth_grant_valid_parameters"
|
35
|
+
|
36
|
+
error_flash "Please authorize to continue", "require_authorization"
|
37
|
+
error_flash "There was an error registering your oauth application", "create_oauth_application"
|
38
|
+
notice_flash "Your oauth application has been registered", "create_oauth_application"
|
39
|
+
|
40
|
+
notice_flash "The oauth token has been revoked", "revoke_oauth_token"
|
41
|
+
|
42
|
+
view "oauth_authorize", "Authorize", "authorize"
|
43
|
+
view "oauth_applications", "Oauth Applications", "oauth_applications"
|
44
|
+
view "oauth_application", "Oauth Application", "oauth_application"
|
45
|
+
view "new_oauth_application", "New Oauth Application", "new_oauth_application"
|
46
|
+
view "oauth_tokens", "Oauth Tokens", "oauth_tokens"
|
47
|
+
|
48
|
+
auth_value_method :json_response_content_type, "application/json"
|
49
|
+
|
50
|
+
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
|
51
|
+
auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
|
52
|
+
auth_value_method :use_oauth_implicit_grant_type, false
|
53
|
+
|
54
|
+
# URL PARAMS
|
55
|
+
|
56
|
+
# Authorize / token
|
57
|
+
%w[
|
58
|
+
grant_type code refresh_token client_id scope
|
59
|
+
state redirect_uri scopes token_type_hint token
|
60
|
+
access_type response_type
|
61
|
+
].each do |param|
|
62
|
+
auth_value_method :"#{param}_param", param
|
63
|
+
end
|
64
|
+
|
65
|
+
# Application
|
66
|
+
APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri].freeze
|
67
|
+
auth_value_method :oauth_application_required_params, APPLICATION_REQUIRED_PARAMS
|
68
|
+
|
69
|
+
(APPLICATION_REQUIRED_PARAMS + %w[client_id client_secret]).each do |param|
|
70
|
+
auth_value_method :"oauth_application_#{param}_param", param
|
71
|
+
end
|
72
|
+
|
73
|
+
# OAuth Token
|
74
|
+
auth_value_method :oauth_tokens_path, "oauth-tokens"
|
75
|
+
auth_value_method :oauth_tokens_table, :oauth_tokens
|
76
|
+
auth_value_method :oauth_tokens_id_column, :id
|
77
|
+
|
78
|
+
%i[
|
79
|
+
oauth_application_id oauth_token_id oauth_grant_id account_id
|
80
|
+
token refresh_token scopes
|
81
|
+
expires_in revoked_at
|
82
|
+
].each do |column|
|
83
|
+
auth_value_method :"oauth_tokens_#{column}_column", column
|
84
|
+
end
|
85
|
+
|
86
|
+
# OAuth Grants
|
87
|
+
auth_value_method :oauth_grants_table, :oauth_grants
|
88
|
+
auth_value_method :oauth_grants_id_column, :id
|
89
|
+
%i[
|
90
|
+
account_id oauth_application_id
|
91
|
+
redirect_uri code scopes access_type
|
92
|
+
expires_in revoked_at
|
93
|
+
].each do |column|
|
94
|
+
auth_value_method :"oauth_grants_#{column}_column", column
|
95
|
+
end
|
96
|
+
|
97
|
+
auth_value_method :authorization_required_error_status, 401
|
98
|
+
auth_value_method :invalid_oauth_response_status, 400
|
99
|
+
|
100
|
+
# OAuth Applications
|
101
|
+
auth_value_method :oauth_applications_path, "oauth-applications"
|
102
|
+
auth_value_method :oauth_applications_table, :oauth_applications
|
103
|
+
|
104
|
+
auth_value_method :oauth_applications_id_column, :id
|
105
|
+
auth_value_method :oauth_applications_id_pattern, Integer
|
106
|
+
|
107
|
+
%i[
|
108
|
+
account_id
|
109
|
+
name description scopes
|
110
|
+
client_id client_secret
|
111
|
+
homepage_url redirect_uri
|
112
|
+
].each do |column|
|
113
|
+
auth_value_method :"oauth_applications_#{column}_column", column
|
114
|
+
end
|
115
|
+
|
116
|
+
auth_value_method :oauth_application_default_scope, SCOPES.first
|
117
|
+
auth_value_method :oauth_application_scopes, SCOPES
|
118
|
+
auth_value_method :oauth_token_type, "Bearer"
|
119
|
+
|
120
|
+
auth_value_method :invalid_request, "Request is missing a required parameter"
|
121
|
+
auth_value_method :invalid_client, "Invalid client"
|
122
|
+
auth_value_method :unauthorized_client, "Unauthorized client"
|
123
|
+
auth_value_method :invalid_grant_type_message, "Invalid grant type"
|
124
|
+
auth_value_method :invalid_grant_message, "Invalid grant"
|
125
|
+
auth_value_method :invalid_scope_message, "Invalid scope"
|
126
|
+
|
127
|
+
auth_value_method :invalid_url_message, "Invalid URL"
|
128
|
+
auth_value_method :unsupported_token_type_message, "Invalid token type hint"
|
129
|
+
|
130
|
+
auth_value_method :unique_error_message, "is already in use"
|
131
|
+
auth_value_method :null_error_message, "is not filled"
|
132
|
+
|
133
|
+
auth_value_methods(
|
134
|
+
:oauth_unique_id_generator
|
135
|
+
)
|
136
|
+
|
137
|
+
redirect(:oauth_application) do |id|
|
138
|
+
"/#{oauth_applications_path}/#{id}"
|
139
|
+
end
|
140
|
+
|
141
|
+
redirect(:require_authorization) do
|
142
|
+
if logged_in?
|
143
|
+
oauth_authorize_path
|
144
|
+
else
|
145
|
+
login_redirect
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
auth_value_method :json_request_accept_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
|
150
|
+
auth_methods(:json_request?)
|
151
|
+
|
152
|
+
def check_csrf?
|
153
|
+
case request.path
|
154
|
+
when oauth_token_path
|
155
|
+
false
|
156
|
+
when oauth_revoke_path
|
157
|
+
!json_request?
|
158
|
+
else
|
159
|
+
super
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Overrides logged_in?, so that a valid authorization token also authnenticates a request
|
164
|
+
def logged_in?
|
165
|
+
super || authorization_token
|
166
|
+
end
|
167
|
+
|
168
|
+
def json_request?
|
169
|
+
return @json_request if defined?(@json_request)
|
170
|
+
|
171
|
+
@json_request = request.get_header("HTTP_ACCEPT") =~ json_request_accept_regexp
|
172
|
+
end
|
173
|
+
|
174
|
+
attr_reader :oauth_application
|
175
|
+
|
176
|
+
def initialize(scope)
|
177
|
+
@scope = scope
|
178
|
+
end
|
179
|
+
|
180
|
+
def state
|
181
|
+
state = param(state_param)
|
182
|
+
|
183
|
+
return unless state && !state.empty?
|
184
|
+
|
185
|
+
state
|
186
|
+
end
|
187
|
+
|
188
|
+
def scopes
|
189
|
+
scopes = param(scopes_param)
|
190
|
+
|
191
|
+
return [oauth_application_default_scope] unless scopes && !scopes.empty?
|
192
|
+
|
193
|
+
scopes.split(" ")
|
194
|
+
end
|
195
|
+
|
196
|
+
def client_id
|
197
|
+
client_id = param(client_id_param)
|
198
|
+
|
199
|
+
return unless client_id && !client_id.empty?
|
200
|
+
|
201
|
+
client_id
|
202
|
+
end
|
203
|
+
|
204
|
+
def redirect_uri
|
205
|
+
redirect_uri = param(redirect_uri_param)
|
206
|
+
|
207
|
+
return oauth_application[oauth_applications_redirect_uri_column] unless redirect_uri && !redirect_uri.empty?
|
208
|
+
|
209
|
+
redirect_uri
|
210
|
+
end
|
211
|
+
|
212
|
+
def token_type_hint
|
213
|
+
token_type_hint = param(token_type_hint_param)
|
214
|
+
|
215
|
+
return "access_token" unless token_type_hint && !token_type_hint.empty?
|
216
|
+
|
217
|
+
token_type_hint
|
218
|
+
end
|
219
|
+
|
220
|
+
def token
|
221
|
+
token = param(token_param)
|
222
|
+
|
223
|
+
return unless token && !token.empty?
|
224
|
+
|
225
|
+
token
|
226
|
+
end
|
227
|
+
|
228
|
+
def oauth_application
|
229
|
+
return @oauth_application if defined?(@oauth_application)
|
230
|
+
|
231
|
+
@oauth_application = begin
|
232
|
+
client_id = param(client_id_param)
|
233
|
+
|
234
|
+
return unless client_id
|
235
|
+
|
236
|
+
db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def authorization_token
|
241
|
+
return @authorization_token if defined?(@authorization_token)
|
242
|
+
|
243
|
+
@authorization_token = begin
|
244
|
+
value = request.get_header("HTTP_AUTHORIZATION").to_s
|
245
|
+
|
246
|
+
scheme, token = value.split(" ", 2)
|
247
|
+
|
248
|
+
return unless scheme == "Bearer"
|
249
|
+
|
250
|
+
# check if there is a token
|
251
|
+
# check if token has not expired
|
252
|
+
# check if token has been revoked
|
253
|
+
db[oauth_tokens_table].where(oauth_tokens_token_column => token)
|
254
|
+
.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
255
|
+
.where(oauth_tokens_revoked_at_column => nil)
|
256
|
+
.first
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def require_oauth_authorization(*scopes)
|
261
|
+
authorization_required unless authorization_token
|
262
|
+
|
263
|
+
scopes << oauth_application_default_scope if scopes.empty?
|
264
|
+
|
265
|
+
token_scopes = authorization_token[:scopes].split(",")
|
266
|
+
|
267
|
+
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
268
|
+
end
|
269
|
+
|
270
|
+
# /oauth-applications routes
|
271
|
+
def oauth_applications
|
272
|
+
request.on(oauth_applications_path) do
|
273
|
+
require_account
|
274
|
+
|
275
|
+
request.get "new" do
|
276
|
+
new_oauth_application_view
|
277
|
+
end
|
278
|
+
request.on(oauth_applications_id_pattern) do |id|
|
279
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
|
280
|
+
scope.instance_variable_set(:@oauth_application, oauth_application)
|
281
|
+
|
282
|
+
request.is do
|
283
|
+
request.get do
|
284
|
+
oauth_application_view
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
request.on(oauth_tokens_path) do
|
289
|
+
oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
|
290
|
+
scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
|
291
|
+
oauth_tokens_view
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
request.get do
|
296
|
+
scope.instance_variable_set(:@oauth_applications, db[:oauth_applications])
|
297
|
+
oauth_applications_view
|
298
|
+
end
|
299
|
+
|
300
|
+
request.post do
|
301
|
+
catch_error do
|
302
|
+
validate_oauth_application_params
|
303
|
+
|
304
|
+
transaction do
|
305
|
+
before_create_oauth_application
|
306
|
+
id = create_oauth_application
|
307
|
+
after_create_oauth_application
|
308
|
+
set_notice_flash create_oauth_application_notice_flash
|
309
|
+
redirect oauth_application_redirect(id)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
set_error_flash create_oauth_application_error_flash
|
313
|
+
new_oauth_application_view
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
private
|
319
|
+
|
320
|
+
def oauth_unique_id_generator
|
321
|
+
SecureRandom.uuid
|
322
|
+
end
|
323
|
+
|
324
|
+
# Oauth Application
|
325
|
+
|
326
|
+
def oauth_application_params
|
327
|
+
@oauth_application_params ||= oauth_application_required_params.each_with_object({}) do |param, params|
|
328
|
+
value = request.params[__send__(:"oauth_application_#{param}_param")]
|
329
|
+
if value && !value.empty?
|
330
|
+
params[param] = value
|
331
|
+
else
|
332
|
+
set_field_error(param, null_error_message)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def validate_oauth_application_params
|
338
|
+
oauth_application_params.each do |key, value|
|
339
|
+
if key == oauth_application_homepage_url_param ||
|
340
|
+
key == oauth_application_redirect_uri_param
|
341
|
+
|
342
|
+
set_field_error(key, invalid_url_message) unless URI::DEFAULT_PARSER.make_regexp(%w[http https]).match?(value)
|
343
|
+
|
344
|
+
elsif key == oauth_application_scopes_param
|
345
|
+
|
346
|
+
value.each do |scope|
|
347
|
+
set_field_error(key, invalid_scope_message) unless oauth_application_scopes.include?(scope)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
throw :rodauth_error if @field_errors && !@field_errors.empty?
|
353
|
+
end
|
354
|
+
|
355
|
+
def create_oauth_application
|
356
|
+
create_params = {
|
357
|
+
oauth_applications_account_id_column => account_id,
|
358
|
+
oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
|
359
|
+
oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
|
360
|
+
oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
|
361
|
+
oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param],
|
362
|
+
oauth_applications_redirect_uri_column => oauth_application_params[oauth_application_redirect_uri_param]
|
363
|
+
}
|
364
|
+
|
365
|
+
# set client ID/secret pairs
|
366
|
+
create_params.merge! \
|
367
|
+
oauth_applications_client_id_column => oauth_unique_id_generator,
|
368
|
+
oauth_applications_client_secret_column => oauth_unique_id_generator
|
369
|
+
|
370
|
+
create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column]
|
371
|
+
create_params[oauth_applications_scopes_column].join(",")
|
372
|
+
else
|
373
|
+
oauth_application_default_scope
|
374
|
+
end
|
375
|
+
|
376
|
+
ds = db[oauth_applications_table]
|
377
|
+
|
378
|
+
id = nil
|
379
|
+
raised = begin
|
380
|
+
id = if ds.supports_returning?(:insert)
|
381
|
+
ds.returning(oauth_applications_id_column).insert(create_params)
|
382
|
+
else
|
383
|
+
id = db[oauth_applications_table].insert(create_params)
|
384
|
+
db[oauth_applications_table].where(oauth_applications_id_column => id).get(oauth_applications_id_column)
|
385
|
+
end
|
386
|
+
false
|
387
|
+
rescue Sequel::ConstraintViolation => e
|
388
|
+
e
|
389
|
+
end
|
390
|
+
|
391
|
+
if raised
|
392
|
+
field = raised.message[/\.(.*)$/, 1]
|
393
|
+
case raised
|
394
|
+
when Sequel::UniqueConstraintViolation
|
395
|
+
throw_error(field, unique_error_message)
|
396
|
+
when Sequel::NotNullConstraintViolation
|
397
|
+
throw_error(field, null_error_message)
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
!raised && id
|
402
|
+
end
|
403
|
+
|
404
|
+
# Authorize
|
405
|
+
|
406
|
+
def validate_oauth_grant_params
|
407
|
+
unless oauth_application && check_valid_redirect_uri? && check_valid_access_type?
|
408
|
+
redirect_response_error("invalid_request")
|
409
|
+
end
|
410
|
+
redirect_response_error("invalid_scope") unless check_valid_scopes?
|
411
|
+
end
|
412
|
+
|
413
|
+
def create_oauth_grant
|
414
|
+
create_params = {
|
415
|
+
oauth_grants_account_id_column => account_id,
|
416
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
417
|
+
oauth_grants_redirect_uri_column => redirect_uri,
|
418
|
+
oauth_grants_code_column => oauth_unique_id_generator,
|
419
|
+
oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
|
420
|
+
oauth_grants_scopes_column => scopes.join(",")
|
421
|
+
}
|
422
|
+
|
423
|
+
unless (access_type = param("access_type")).empty?
|
424
|
+
create_params[oauth_grants_access_type_column] = access_type
|
425
|
+
end
|
426
|
+
|
427
|
+
ds = db[oauth_grants_table]
|
428
|
+
|
429
|
+
begin
|
430
|
+
if ds.supports_returning?(:insert)
|
431
|
+
ds.returning(authorize_code_column).insert(create_params)
|
432
|
+
else
|
433
|
+
id = ds.insert(create_params)
|
434
|
+
ds.where(oauth_grants_id_column => id).get(oauth_grants_code_column)
|
435
|
+
end
|
436
|
+
rescue Sequel::UniqueConstraintViolation
|
437
|
+
retry
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
# Access Tokens
|
442
|
+
|
443
|
+
def validate_oauth_token_params
|
444
|
+
redirect_response_error("invalid_request") unless param(client_id_param)
|
445
|
+
|
446
|
+
unless (grant_type = param(grant_type_param))
|
447
|
+
redirect_response_error("invalid_request")
|
448
|
+
end
|
449
|
+
|
450
|
+
case grant_type
|
451
|
+
when "authorization_code"
|
452
|
+
redirect_response_error("invalid_request") unless param(code_param)
|
453
|
+
|
454
|
+
when "refresh_token"
|
455
|
+
redirect_response_error("invalid_request") unless param(refresh_token_param)
|
456
|
+
else
|
457
|
+
redirect_response_error("invalid_request")
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
def generate_oauth_token(params = {})
|
462
|
+
create_params = {
|
463
|
+
oauth_grants_expires_in_column => Time.now + oauth_token_expires_in,
|
464
|
+
oauth_tokens_token_column => oauth_unique_id_generator
|
465
|
+
}.merge(params)
|
466
|
+
|
467
|
+
ds = db[oauth_tokens_table]
|
468
|
+
|
469
|
+
begin
|
470
|
+
if ds.supports_returning?(:insert)
|
471
|
+
ds.returning.insert(create_params)
|
472
|
+
else
|
473
|
+
id = ds.insert(create_params)
|
474
|
+
ds.where(oauth_tokens_id_column => id).first
|
475
|
+
end
|
476
|
+
rescue Sequel::UniqueConstraintViolation
|
477
|
+
retry
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
def create_oauth_token
|
482
|
+
case param(grant_type_param)
|
483
|
+
when "authorization_code"
|
484
|
+
# fetch oauth grant
|
485
|
+
oauth_grant = db[oauth_grants_table].where(
|
486
|
+
oauth_grants_code_column => param(code_param),
|
487
|
+
oauth_grants_redirect_uri_column => param(redirect_uri_param),
|
488
|
+
oauth_grants_oauth_application_id_column => db[oauth_applications_table].where(
|
489
|
+
oauth_applications_client_id_column => param(client_id_param),
|
490
|
+
oauth_applications_account_id_column => oauth_applications_account_id_column
|
491
|
+
).select(oauth_applications_id_column)
|
492
|
+
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
493
|
+
.where(oauth_grants_revoked_at_column => nil)
|
494
|
+
.first
|
495
|
+
|
496
|
+
redirect_response_error("invalid_grant") unless oauth_grant
|
497
|
+
|
498
|
+
create_params = {
|
499
|
+
oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
|
500
|
+
oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
|
501
|
+
oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
|
502
|
+
oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
|
503
|
+
}
|
504
|
+
|
505
|
+
if oauth_grant[oauth_grants_access_type_column] == "offline"
|
506
|
+
create_params[oauth_tokens_refresh_token_column] = oauth_unique_id_generator
|
507
|
+
end
|
508
|
+
# revoke oauth grant
|
509
|
+
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
|
510
|
+
.update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
511
|
+
|
512
|
+
generate_oauth_token(create_params)
|
513
|
+
when "refresh_token"
|
514
|
+
# fetch oauth grant
|
515
|
+
oauth_token = db[oauth_tokens_table].where(
|
516
|
+
oauth_tokens_refresh_token_column => param(refresh_token_param),
|
517
|
+
oauth_tokens_oauth_application_id_column => db[oauth_applications_table].where(
|
518
|
+
oauth_applications_client_id_column => param(client_id_param),
|
519
|
+
oauth_applications_account_id_column => account_id
|
520
|
+
).select(oauth_applications_id_column)
|
521
|
+
).where(oauth_grants_revoked_at_column => nil).first
|
522
|
+
|
523
|
+
redirect_response_error("invalid_grant") unless oauth_token
|
524
|
+
|
525
|
+
update_params = {
|
526
|
+
oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
|
527
|
+
oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in,
|
528
|
+
oauth_tokens_token_column => oauth_unique_id_generator
|
529
|
+
}
|
530
|
+
|
531
|
+
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
532
|
+
begin
|
533
|
+
if ds.supports_returning?(:update)
|
534
|
+
ds.returning.update(update_params)
|
535
|
+
else
|
536
|
+
ds.update(update_params)
|
537
|
+
ds.first
|
538
|
+
end
|
539
|
+
rescue Sequel::UniqueConstraintViolation
|
540
|
+
retry
|
541
|
+
end
|
542
|
+
else
|
543
|
+
redirect_response_error("invalid_grant")
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
# Token revocation
|
548
|
+
|
549
|
+
TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
|
550
|
+
|
551
|
+
def validate_oauth_revoke_params
|
552
|
+
# check if valid token hint type
|
553
|
+
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(token_type_hint)
|
554
|
+
|
555
|
+
redirect_response_error("invalid_request") unless param(token_param)
|
556
|
+
end
|
557
|
+
|
558
|
+
def revoke_oauth_token
|
559
|
+
# one can only revoke tokens which haven't been revoked before, and which are
|
560
|
+
# either our tokens, or tokens from applications we own.
|
561
|
+
ds = db[oauth_tokens_table]
|
562
|
+
.where(oauth_tokens_revoked_at_column => nil)
|
563
|
+
.where(
|
564
|
+
Sequel.or(
|
565
|
+
oauth_tokens_account_id_column => account_id,
|
566
|
+
oauth_tokens_oauth_application_id_column => db[oauth_applications_table].where(
|
567
|
+
oauth_applications_client_id_column => param(client_id_param),
|
568
|
+
oauth_applications_account_id_column => account_id
|
569
|
+
).select(oauth_applications_id_column)
|
570
|
+
)
|
571
|
+
)
|
572
|
+
ds = case token_type_hint
|
573
|
+
when "access_token"
|
574
|
+
ds.where(oauth_tokens_token_column => token)
|
575
|
+
when "refresh_token"
|
576
|
+
ds.where(oauth_tokens_refresh_token_column => token)
|
577
|
+
end
|
578
|
+
|
579
|
+
oauth_token = ds.first
|
580
|
+
redirect_response_error("invalid_request") unless oauth_token
|
581
|
+
|
582
|
+
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
|
583
|
+
|
584
|
+
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
585
|
+
|
586
|
+
if ds.supports_returning?(:update)
|
587
|
+
ds.returning.update(update_params)
|
588
|
+
else
|
589
|
+
ds.update(update_params)
|
590
|
+
ds.first
|
591
|
+
end
|
592
|
+
|
593
|
+
# If the particular
|
594
|
+
# token is a refresh token and the authorization server supports the
|
595
|
+
# revocation of access tokens, then the authorization server SHOULD
|
596
|
+
# also invalidate all access tokens based on the same authorization
|
597
|
+
# grant
|
598
|
+
#
|
599
|
+
# we don't need to do anything here, as we revalidate existing tokens
|
600
|
+
end
|
601
|
+
|
602
|
+
# Response helpers
|
603
|
+
|
604
|
+
def redirect_response_error(error_code, redirect_url = request.referer || default_redirect)
|
605
|
+
if json_request?
|
606
|
+
throw_json_response_error(invalid_oauth_response_status, error_code)
|
607
|
+
else
|
608
|
+
redirect_url = URI.parse(redirect_url)
|
609
|
+
query_params = ["error=#{error_code}"]
|
610
|
+
if respond_to?(:"#{error_code}_message")
|
611
|
+
message = send(:"#{error_code}_message")
|
612
|
+
query_params << ["error_description=#{CGI.escape(message)}"]
|
613
|
+
end
|
614
|
+
query_params << redirect_url.query if redirect_url.query
|
615
|
+
redirect_url.query = query_params.join("&")
|
616
|
+
redirect(redirect_url.to_s)
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
def throw_json_response_error(status, error_code)
|
621
|
+
set_response_error_status(status)
|
622
|
+
payload = { "error" => error_code }
|
623
|
+
payload["error_description"] = send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")
|
624
|
+
json_payload = if request.respond_to?(:convert_to_json)
|
625
|
+
request.send(:convert_to_json, payload)
|
626
|
+
else
|
627
|
+
JSON.dump(payload)
|
628
|
+
end
|
629
|
+
response["Content-Type"] ||= json_response_content_type
|
630
|
+
response["WWW-Authenticate"] = "Bearer" if status == 401
|
631
|
+
response.write(json_payload)
|
632
|
+
request.halt
|
633
|
+
end
|
634
|
+
|
635
|
+
def authorization_required
|
636
|
+
if json_request?
|
637
|
+
throw_json_response_error(authorization_required_error_status, "invalid_client")
|
638
|
+
else
|
639
|
+
set_redirect_error_flash(require_authorization_error_flash)
|
640
|
+
redirect(require_authorization_redirect)
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
def check_valid_scopes?
|
645
|
+
return false unless scopes
|
646
|
+
|
647
|
+
(scopes - oauth_application[oauth_applications_scopes_column].split(",")).empty?
|
648
|
+
end
|
649
|
+
|
650
|
+
def check_valid_redirect_uri?
|
651
|
+
redirect_uri == oauth_application[oauth_applications_redirect_uri_column]
|
652
|
+
end
|
653
|
+
|
654
|
+
ACCESS_TYPES = %w[offline online].freeze
|
655
|
+
|
656
|
+
def check_valid_access_type?
|
657
|
+
access_type = param("access_type")
|
658
|
+
access_type.empty? || ACCESS_TYPES.include?(access_type)
|
659
|
+
end
|
660
|
+
|
661
|
+
def check_valid_response_type?
|
662
|
+
response_type = param("response_type")
|
663
|
+
|
664
|
+
return true if response_type.empty? || response_type == "code"
|
665
|
+
|
666
|
+
return use_oauth_implicit_grant_type if response_type == "token"
|
667
|
+
|
668
|
+
false
|
669
|
+
end
|
670
|
+
|
671
|
+
# /oauth-token
|
672
|
+
route(:oauth_token) do |r|
|
673
|
+
throw_json_response_error(authorization_required_error_status, "invalid_client") unless logged_in?
|
674
|
+
|
675
|
+
# access-token
|
676
|
+
r.post do
|
677
|
+
catch_error do
|
678
|
+
validate_oauth_token_params
|
679
|
+
|
680
|
+
oauth_token = nil
|
681
|
+
transaction do
|
682
|
+
before_token
|
683
|
+
oauth_token = create_oauth_token
|
684
|
+
after_token
|
685
|
+
end
|
686
|
+
|
687
|
+
response.status = 200
|
688
|
+
response["Content-Type"] ||= json_response_content_type
|
689
|
+
json_response = {
|
690
|
+
"token" => oauth_token[:token],
|
691
|
+
"token_type" => oauth_token_type,
|
692
|
+
"expires_in" => oauth_token_expires_in
|
693
|
+
}
|
694
|
+
|
695
|
+
json_response["refresh_token"] = oauth_token[:refresh_token] if oauth_token[:refresh_token]
|
696
|
+
|
697
|
+
json_payload = if request.respond_to?(:convert_to_json)
|
698
|
+
request.send(:convert_to_json, json_response)
|
699
|
+
else
|
700
|
+
JSON.dump(json_response)
|
701
|
+
end
|
702
|
+
response.write(json_payload)
|
703
|
+
request.halt
|
704
|
+
end
|
705
|
+
|
706
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
# /oauth-revoke
|
711
|
+
route(:oauth_revoke) do |r|
|
712
|
+
require_account
|
713
|
+
|
714
|
+
# access-token
|
715
|
+
r.post do
|
716
|
+
catch_error do
|
717
|
+
validate_oauth_revoke_params
|
718
|
+
|
719
|
+
oauth_token = nil
|
720
|
+
transaction do
|
721
|
+
before_revoke
|
722
|
+
oauth_token = revoke_oauth_token
|
723
|
+
after_revoke
|
724
|
+
end
|
725
|
+
|
726
|
+
if json_request?
|
727
|
+
response.status = 200
|
728
|
+
response["Content-Type"] ||= json_response_content_type
|
729
|
+
json_response = {
|
730
|
+
"token" => oauth_token[:token],
|
731
|
+
"refresh_token" => oauth_token[:refresh_token],
|
732
|
+
"revoked_at" => oauth_token[:revoked_at]
|
733
|
+
}
|
734
|
+
json_payload = if request.respond_to?(:convert_to_json)
|
735
|
+
request.send(:convert_to_json, json_response)
|
736
|
+
else
|
737
|
+
JSON.dump(json_response)
|
738
|
+
end
|
739
|
+
response.write(json_payload)
|
740
|
+
request.halt
|
741
|
+
else
|
742
|
+
set_notice_flash revoke_oauth_token_notice_flash
|
743
|
+
redirect request.referer || "/"
|
744
|
+
end
|
745
|
+
end
|
746
|
+
|
747
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
748
|
+
end
|
749
|
+
end
|
750
|
+
|
751
|
+
# /oauth-authorize
|
752
|
+
route(:oauth_authorize) do |r|
|
753
|
+
require_account
|
754
|
+
|
755
|
+
r.get do
|
756
|
+
validate_oauth_grant_params
|
757
|
+
authorize_view
|
758
|
+
end
|
759
|
+
|
760
|
+
r.post do
|
761
|
+
validate_oauth_grant_params
|
762
|
+
|
763
|
+
code = nil
|
764
|
+
query_params = []
|
765
|
+
fragment_params = []
|
766
|
+
|
767
|
+
transaction do
|
768
|
+
before_authorize
|
769
|
+
case param(response_type_param)
|
770
|
+
when "token"
|
771
|
+
redirect_response_error("invalid_request", redirect_uri) unless use_oauth_implicit_grant_type
|
772
|
+
|
773
|
+
create_params = {
|
774
|
+
oauth_tokens_account_id_column => account_id,
|
775
|
+
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
776
|
+
oauth_tokens_scopes_column => scopes
|
777
|
+
}
|
778
|
+
oauth_token = generate_oauth_token(create_params)
|
779
|
+
|
780
|
+
fragment_params << ["access_token=#{oauth_token[:token]}"]
|
781
|
+
fragment_params << ["token_type=#{oauth_token_type}"]
|
782
|
+
fragment_params << ["expires_in=#{oauth_token_expires_in}"]
|
783
|
+
when "code", "", nil
|
784
|
+
code = create_oauth_grant
|
785
|
+
query_params << ["code=#{code}"]
|
786
|
+
else
|
787
|
+
redirect_response_error("invalid_request")
|
788
|
+
end
|
789
|
+
after_authorize
|
790
|
+
end
|
791
|
+
|
792
|
+
redirect_url = URI.parse(redirect_uri)
|
793
|
+
query_params << "state=#{state}" if state
|
794
|
+
query_params << redirect_url.query if redirect_url.query
|
795
|
+
redirect_url.query = query_params.join("&") unless query_params.empty?
|
796
|
+
redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
|
797
|
+
|
798
|
+
redirect(redirect_url.to_s)
|
799
|
+
end
|
800
|
+
end
|
801
|
+
end
|
802
|
+
end
|