rodauth-oauth 0.0.5 → 0.4.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 +4 -4
- data/CHANGELOG.md +180 -5
- data/README.md +45 -21
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +8 -5
- data/lib/rodauth/features/oauth.rb +514 -416
- data/lib/rodauth/features/oauth_http_mac.rb +6 -10
- data/lib/rodauth/features/oauth_jwt.rb +178 -74
- data/lib/rodauth/features/oauth_saml.rb +104 -0
- data/lib/rodauth/features/oidc.rb +399 -0
- data/lib/rodauth/oauth/database_extensions.rb +73 -0
- data/lib/rodauth/oauth/ttl_store.rb +1 -1
- data/lib/rodauth/oauth/version.rb +1 -1
- data/templates/authorize.str +34 -0
- data/templates/client_secret_field.str +4 -0
- data/templates/description_field.str +4 -0
- data/templates/homepage_url_field.str +4 -0
- data/templates/name_field.str +4 -0
- data/templates/new_oauth_application.str +10 -0
- data/templates/oauth_application.str +11 -0
- data/templates/oauth_applications.str +14 -0
- data/templates/oauth_tokens.str +49 -0
- data/templates/redirect_uri_field.str +4 -0
- data/templates/scope_field.str +10 -0
- metadata +24 -10
@@ -1,16 +1,21 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
|
+
require "time"
|
3
4
|
require "base64"
|
4
5
|
require "securerandom"
|
5
6
|
require "net/http"
|
6
7
|
|
7
8
|
require "rodauth/oauth/ttl_store"
|
9
|
+
require "rodauth/oauth/database_extensions"
|
8
10
|
|
9
11
|
module Rodauth
|
10
12
|
Feature.define(:oauth) do
|
11
13
|
# RUBY EXTENSIONS
|
12
|
-
# :nocov:
|
13
14
|
unless Regexp.method_defined?(:match?)
|
15
|
+
# If you wonder why this is there: the oauth feature uses a refinement to enhance the
|
16
|
+
# Regexp class locally with #match? , but this is never tested, because ActiveSupport
|
17
|
+
# monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
|
18
|
+
# :nocov:
|
14
19
|
module RegexpExtensions
|
15
20
|
refine(Regexp) do
|
16
21
|
def match?(*args)
|
@@ -19,6 +24,7 @@ module Rodauth
|
|
19
24
|
end
|
20
25
|
end
|
21
26
|
using(RegexpExtensions)
|
27
|
+
# :nocov:
|
22
28
|
end
|
23
29
|
|
24
30
|
unless String.method_defined?(:delete_suffix!)
|
@@ -37,13 +43,13 @@ module Rodauth
|
|
37
43
|
end
|
38
44
|
using(SuffixExtensions)
|
39
45
|
end
|
40
|
-
# :nocov:
|
41
46
|
|
42
47
|
SCOPES = %w[profile.read].freeze
|
43
48
|
|
49
|
+
SERVER_METADATA = OAuth::TtlStore.new
|
50
|
+
|
44
51
|
before "authorize"
|
45
52
|
after "authorize"
|
46
|
-
after "authorize_failure"
|
47
53
|
|
48
54
|
before "token"
|
49
55
|
|
@@ -55,15 +61,13 @@ module Rodauth
|
|
55
61
|
before "create_oauth_application"
|
56
62
|
after "create_oauth_application"
|
57
63
|
|
58
|
-
error_flash "OAuth Authorization invalid parameters", "oauth_grant_valid_parameters"
|
59
|
-
|
60
64
|
error_flash "Please authorize to continue", "require_authorization"
|
61
65
|
error_flash "There was an error registering your oauth application", "create_oauth_application"
|
62
66
|
notice_flash "Your oauth application has been registered", "create_oauth_application"
|
63
67
|
|
64
68
|
notice_flash "The oauth token has been revoked", "revoke_oauth_token"
|
65
69
|
|
66
|
-
view "
|
70
|
+
view "authorize", "Authorize", "authorize"
|
67
71
|
view "oauth_applications", "Oauth Applications", "oauth_applications"
|
68
72
|
view "oauth_application", "Oauth Application", "oauth_application"
|
69
73
|
view "new_oauth_application", "New Oauth Application", "new_oauth_application"
|
@@ -73,14 +77,16 @@ module Rodauth
|
|
73
77
|
|
74
78
|
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
|
75
79
|
auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
|
80
|
+
auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
|
76
81
|
auth_value_method :use_oauth_implicit_grant_type?, false
|
77
82
|
auth_value_method :use_oauth_pkce?, true
|
78
83
|
auth_value_method :use_oauth_access_type?, true
|
79
84
|
|
80
85
|
auth_value_method :oauth_require_pkce, false
|
81
86
|
auth_value_method :oauth_pkce_challenge_method, "S256"
|
87
|
+
auth_value_method :oauth_response_mode, "query"
|
82
88
|
|
83
|
-
auth_value_method :oauth_valid_uri_schemes, %w[
|
89
|
+
auth_value_method :oauth_valid_uri_schemes, %w[https]
|
84
90
|
|
85
91
|
auth_value_method :oauth_scope_separator, " "
|
86
92
|
|
@@ -95,6 +101,7 @@ module Rodauth
|
|
95
101
|
button "Register", "oauth_application"
|
96
102
|
button "Authorize", "oauth_authorize"
|
97
103
|
button "Revoke", "oauth_token_revoke"
|
104
|
+
button "Back to Client Application", "oauth_authorize_post"
|
98
105
|
|
99
106
|
# OAuth Token
|
100
107
|
auth_value_method :oauth_tokens_path, "oauth-tokens"
|
@@ -113,6 +120,8 @@ module Rodauth
|
|
113
120
|
auth_value_method :oauth_tokens_token_hash_column, nil
|
114
121
|
auth_value_method :oauth_tokens_refresh_token_hash_column, nil
|
115
122
|
|
123
|
+
# Access Token reuse
|
124
|
+
auth_value_method :oauth_reuse_access_token, false
|
116
125
|
# OAuth Grants
|
117
126
|
auth_value_method :oauth_grants_table, :oauth_grants
|
118
127
|
auth_value_method :oauth_grants_id_column, :id
|
@@ -127,6 +136,7 @@ module Rodauth
|
|
127
136
|
|
128
137
|
auth_value_method :authorization_required_error_status, 401
|
129
138
|
auth_value_method :invalid_oauth_response_status, 400
|
139
|
+
auth_value_method :already_in_use_response_status, 409
|
130
140
|
|
131
141
|
# OAuth Applications
|
132
142
|
auth_value_method :oauth_applications_path, "oauth-applications"
|
@@ -144,13 +154,13 @@ module Rodauth
|
|
144
154
|
auth_value_method :"oauth_applications_#{column}_column", column
|
145
155
|
end
|
146
156
|
|
157
|
+
# Feature options
|
147
158
|
auth_value_method :oauth_application_default_scope, SCOPES.first
|
148
159
|
auth_value_method :oauth_application_scopes, SCOPES
|
149
160
|
auth_value_method :oauth_token_type, "bearer"
|
161
|
+
auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
|
150
162
|
|
151
|
-
auth_value_method :
|
152
|
-
auth_value_method :invalid_client, "Invalid client"
|
153
|
-
auth_value_method :unauthorized_client, "Unauthorized client"
|
163
|
+
auth_value_method :invalid_client_message, "Invalid client"
|
154
164
|
auth_value_method :invalid_grant_type_message, "Invalid grant type"
|
155
165
|
auth_value_method :invalid_grant_message, "Invalid grant"
|
156
166
|
auth_value_method :invalid_scope_message, "Invalid scope"
|
@@ -160,6 +170,8 @@ module Rodauth
|
|
160
170
|
|
161
171
|
auth_value_method :unique_error_message, "is already in use"
|
162
172
|
auth_value_method :null_error_message, "is not filled"
|
173
|
+
auth_value_method :already_in_use_message, "error generating unique token"
|
174
|
+
auth_value_method :already_in_use_error_code, "invalid_request"
|
163
175
|
|
164
176
|
# PKCE
|
165
177
|
auth_value_method :code_challenge_required_error_code, "invalid_request"
|
@@ -177,6 +189,8 @@ module Rodauth
|
|
177
189
|
# Only required to use if the plugin is to be used in a resource server
|
178
190
|
auth_value_method :is_authorization_server?, true
|
179
191
|
|
192
|
+
auth_value_method :oauth_unique_id_generation_retries, 3
|
193
|
+
|
180
194
|
auth_value_methods(
|
181
195
|
:fetch_access_token,
|
182
196
|
:oauth_unique_id_generator,
|
@@ -184,36 +198,231 @@ module Rodauth
|
|
184
198
|
:secret_hash,
|
185
199
|
:generate_token_hash,
|
186
200
|
:authorization_server_url,
|
187
|
-
:before_introspection_request
|
201
|
+
:before_introspection_request,
|
202
|
+
:require_authorizable_account,
|
203
|
+
:oauth_tokens_unique_columns
|
188
204
|
)
|
189
205
|
|
190
206
|
auth_value_methods(:only_json?)
|
191
207
|
|
192
|
-
|
193
|
-
|
208
|
+
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
|
209
|
+
|
210
|
+
# /token
|
211
|
+
route(:token) do |r|
|
212
|
+
next unless is_authorization_server?
|
213
|
+
|
214
|
+
before_token_route
|
215
|
+
require_oauth_application
|
216
|
+
|
217
|
+
r.post do
|
218
|
+
catch_error do
|
219
|
+
validate_oauth_token_params
|
220
|
+
|
221
|
+
oauth_token = nil
|
222
|
+
transaction do
|
223
|
+
before_token
|
224
|
+
oauth_token = create_oauth_token
|
225
|
+
end
|
226
|
+
|
227
|
+
json_response_success(json_access_token_payload(oauth_token))
|
228
|
+
end
|
229
|
+
|
230
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
231
|
+
end
|
194
232
|
end
|
195
233
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
234
|
+
# /introspect
|
235
|
+
route(:introspect) do |r|
|
236
|
+
next unless is_authorization_server?
|
237
|
+
|
238
|
+
before_introspect_route
|
239
|
+
|
240
|
+
r.post do
|
241
|
+
catch_error do
|
242
|
+
validate_oauth_introspect_params
|
243
|
+
|
244
|
+
before_introspect
|
245
|
+
oauth_token = case param("token_type_hint")
|
246
|
+
when "access_token"
|
247
|
+
oauth_token_by_token(param("token"))
|
248
|
+
when "refresh_token"
|
249
|
+
oauth_token_by_refresh_token(param("token"))
|
250
|
+
else
|
251
|
+
oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
|
252
|
+
end
|
253
|
+
|
254
|
+
if oauth_application
|
255
|
+
redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
|
256
|
+
elsif oauth_token
|
257
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
258
|
+
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
259
|
+
end
|
260
|
+
|
261
|
+
json_response_success(json_token_introspect_payload(oauth_token))
|
262
|
+
end
|
263
|
+
|
264
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
203
265
|
end
|
204
266
|
end
|
205
267
|
|
206
|
-
|
268
|
+
# /revoke
|
269
|
+
route(:revoke) do |r|
|
270
|
+
next unless is_authorization_server?
|
207
271
|
|
208
|
-
|
272
|
+
before_revoke_route
|
273
|
+
require_oauth_application
|
274
|
+
|
275
|
+
r.post do
|
276
|
+
catch_error do
|
277
|
+
validate_oauth_revoke_params
|
278
|
+
|
279
|
+
oauth_token = nil
|
280
|
+
transaction do
|
281
|
+
before_revoke
|
282
|
+
oauth_token = revoke_oauth_token
|
283
|
+
after_revoke
|
284
|
+
end
|
285
|
+
|
286
|
+
if accepts_json?
|
287
|
+
json_response_success \
|
288
|
+
"token" => oauth_token[oauth_tokens_token_column],
|
289
|
+
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
|
290
|
+
"revoked_at" => convert_timestamp(oauth_token[oauth_tokens_revoked_at_column])
|
291
|
+
else
|
292
|
+
set_notice_flash revoke_oauth_token_notice_flash
|
293
|
+
redirect request.referer || "/"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
redirect_response_error("invalid_request", request.referer || "/")
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# /authorize
|
302
|
+
route(:authorize) do |r|
|
303
|
+
next unless is_authorization_server?
|
304
|
+
|
305
|
+
before_authorize_route
|
306
|
+
require_authorizable_account
|
307
|
+
|
308
|
+
validate_oauth_grant_params
|
309
|
+
try_approval_prompt if use_oauth_access_type? && request.get?
|
310
|
+
|
311
|
+
r.get do
|
312
|
+
authorize_view
|
313
|
+
end
|
314
|
+
|
315
|
+
r.post do
|
316
|
+
redirect_url = URI.parse(redirect_uri)
|
317
|
+
|
318
|
+
params, mode = transaction do
|
319
|
+
before_authorize
|
320
|
+
do_authorize
|
321
|
+
end
|
322
|
+
|
323
|
+
case mode
|
324
|
+
when "query"
|
325
|
+
params = params.map { |k, v| "#{k}=#{v}" }
|
326
|
+
params << redirect_url.query if redirect_url.query
|
327
|
+
redirect_url.query = params.join("&")
|
328
|
+
redirect(redirect_url.to_s)
|
329
|
+
when "fragment"
|
330
|
+
params = params.map { |k, v| "#{k}=#{v}" }
|
331
|
+
params << redirect_url.query if redirect_url.query
|
332
|
+
redirect_url.fragment = params.join("&")
|
333
|
+
redirect(redirect_url.to_s)
|
334
|
+
when "form_post"
|
335
|
+
scope.view layout: false, inline: <<-FORM
|
336
|
+
<html>
|
337
|
+
<head><title>Authorized</title></head>
|
338
|
+
<body onload="javascript:document.forms[0].submit()">
|
339
|
+
<form method="post" action="#{redirect_uri}">
|
340
|
+
#{
|
341
|
+
params.map do |name, value|
|
342
|
+
"<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
|
343
|
+
end.join
|
344
|
+
}
|
345
|
+
<input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
|
346
|
+
</form>
|
347
|
+
</body>
|
348
|
+
</html>
|
349
|
+
FORM
|
350
|
+
when "none"
|
351
|
+
redirect(redirect_url.to_s)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
def oauth_server_metadata(issuer = nil)
|
357
|
+
request.on(".well-known") do
|
358
|
+
request.on("oauth-authorization-server") do
|
359
|
+
request.get do
|
360
|
+
json_response_success(oauth_server_metadata_body(issuer), true)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
# /oauth-applications routes
|
367
|
+
def oauth_applications
|
368
|
+
request.on(oauth_applications_path) do
|
369
|
+
require_account
|
370
|
+
|
371
|
+
request.get "new" do
|
372
|
+
new_oauth_application_view
|
373
|
+
end
|
374
|
+
|
375
|
+
request.on(oauth_applications_id_pattern) do |id|
|
376
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
|
377
|
+
next unless oauth_application
|
378
|
+
|
379
|
+
scope.instance_variable_set(:@oauth_application, oauth_application)
|
380
|
+
|
381
|
+
request.is do
|
382
|
+
request.get do
|
383
|
+
oauth_application_view
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
request.on(oauth_tokens_path) do
|
388
|
+
oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
|
389
|
+
scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
|
390
|
+
request.get do
|
391
|
+
oauth_tokens_view
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
request.get do
|
397
|
+
scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table])
|
398
|
+
oauth_applications_view
|
399
|
+
end
|
400
|
+
|
401
|
+
request.post do
|
402
|
+
catch_error do
|
403
|
+
validate_oauth_application_params
|
404
|
+
|
405
|
+
transaction do
|
406
|
+
before_create_oauth_application
|
407
|
+
id = create_oauth_application
|
408
|
+
after_create_oauth_application
|
409
|
+
set_notice_flash create_oauth_application_notice_flash
|
410
|
+
redirect "#{request.path}/#{id}"
|
411
|
+
end
|
412
|
+
end
|
413
|
+
set_error_flash create_oauth_application_error_flash
|
414
|
+
new_oauth_application_view
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
209
418
|
|
210
419
|
def check_csrf?
|
211
420
|
case request.path
|
212
|
-
when
|
421
|
+
when token_path, introspect_path
|
213
422
|
false
|
214
|
-
when
|
423
|
+
when revoke_path
|
215
424
|
!json_request?
|
216
|
-
when
|
425
|
+
when authorize_path, %r{/#{oauth_applications_path}}
|
217
426
|
only_json? ? false : super
|
218
427
|
else
|
219
428
|
super
|
@@ -232,30 +441,28 @@ module Rodauth
|
|
232
441
|
end
|
233
442
|
|
234
443
|
unless method_defined?(:json_request?)
|
235
|
-
# :nocov:
|
236
444
|
# copied from the jwt feature
|
237
445
|
def json_request?
|
238
446
|
return @json_request if defined?(@json_request)
|
239
447
|
|
240
448
|
@json_request = request.content_type =~ json_request_regexp
|
241
449
|
end
|
242
|
-
# :nocov:
|
243
450
|
end
|
244
451
|
|
245
452
|
def initialize(scope)
|
246
453
|
@scope = scope
|
247
454
|
end
|
248
455
|
|
249
|
-
def state
|
250
|
-
param_or_nil("state")
|
251
|
-
end
|
252
|
-
|
253
456
|
def scopes
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
457
|
+
scope = request.params["scope"]
|
458
|
+
case scope
|
459
|
+
when Array
|
460
|
+
scope
|
461
|
+
when String
|
462
|
+
scope.split(" ")
|
463
|
+
when nil
|
464
|
+
[oauth_application_default_scope]
|
465
|
+
end
|
259
466
|
end
|
260
467
|
|
261
468
|
def redirect_uri
|
@@ -267,14 +474,6 @@ module Rodauth
|
|
267
474
|
end
|
268
475
|
end
|
269
476
|
|
270
|
-
def token_type_hint
|
271
|
-
param_or_nil("token_type_hint") || "access_token"
|
272
|
-
end
|
273
|
-
|
274
|
-
def token
|
275
|
-
param_or_nil("token")
|
276
|
-
end
|
277
|
-
|
278
477
|
def oauth_application
|
279
478
|
return @oauth_application if defined?(@oauth_application)
|
280
479
|
|
@@ -296,6 +495,8 @@ module Rodauth
|
|
296
495
|
|
297
496
|
return unless scheme.downcase == oauth_token_type
|
298
497
|
|
498
|
+
return if token.empty?
|
499
|
+
|
299
500
|
token
|
300
501
|
end
|
301
502
|
|
@@ -337,67 +538,46 @@ module Rodauth
|
|
337
538
|
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
338
539
|
end
|
339
540
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
require_account
|
541
|
+
def post_configure
|
542
|
+
super
|
543
|
+
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
|
344
544
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
|
351
|
-
scope.instance_variable_set(:@oauth_application, oauth_application)
|
352
|
-
|
353
|
-
request.is do
|
354
|
-
request.get do
|
355
|
-
oauth_application_view
|
356
|
-
end
|
357
|
-
end
|
358
|
-
|
359
|
-
request.on(oauth_tokens_path) do
|
360
|
-
oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
|
361
|
-
scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
|
362
|
-
oauth_tokens_view
|
363
|
-
end
|
545
|
+
# Check whether we can reutilize db entries for the same account / application pair
|
546
|
+
one_oauth_token_per_account = begin
|
547
|
+
db.indexes(oauth_tokens_table).values.any? do |definition|
|
548
|
+
definition[:unique] &&
|
549
|
+
definition[:columns] == oauth_tokens_unique_columns
|
364
550
|
end
|
551
|
+
end
|
552
|
+
self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
|
553
|
+
end
|
365
554
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
end
|
555
|
+
def use_date_arithmetic?
|
556
|
+
true
|
557
|
+
end
|
370
558
|
|
371
|
-
|
372
|
-
catch_error do
|
373
|
-
validate_oauth_application_params
|
559
|
+
private
|
374
560
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
set_error_flash create_oauth_application_error_flash
|
384
|
-
new_oauth_application_view
|
385
|
-
end
|
561
|
+
def rescue_from_uniqueness_error(&block)
|
562
|
+
retries = oauth_unique_id_generation_retries
|
563
|
+
begin
|
564
|
+
transaction(savepoint: :only, &block)
|
565
|
+
rescue Sequel::UniqueConstraintViolation
|
566
|
+
redirect_response_error("already_in_use") if retries.zero?
|
567
|
+
retries -= 1
|
568
|
+
retry
|
386
569
|
end
|
387
570
|
end
|
388
571
|
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
end
|
572
|
+
# OAuth Token Unique/Reuse
|
573
|
+
def oauth_tokens_unique_columns
|
574
|
+
[
|
575
|
+
oauth_tokens_oauth_application_id_column,
|
576
|
+
oauth_tokens_account_id_column,
|
577
|
+
oauth_tokens_scopes_column
|
578
|
+
]
|
397
579
|
end
|
398
580
|
|
399
|
-
private
|
400
|
-
|
401
581
|
def authorization_server_url
|
402
582
|
base_url
|
403
583
|
end
|
@@ -420,10 +600,10 @@ module Rodauth
|
|
420
600
|
|
421
601
|
# time-to-live
|
422
602
|
ttl = if response.key?("cache-control")
|
423
|
-
cache_control = response["
|
424
|
-
cache_control[/max-age=(\d+)/, 1]
|
603
|
+
cache_control = response["cache-control"]
|
604
|
+
cache_control[/max-age=(\d+)/, 1].to_i
|
425
605
|
elsif response.key?("expires")
|
426
|
-
Time.
|
606
|
+
Time.parse(response["expires"]).to_i - Time.now.to_i
|
427
607
|
end
|
428
608
|
|
429
609
|
[JSON.parse(response.body, symbolize_names: true), ttl]
|
@@ -435,7 +615,7 @@ module Rodauth
|
|
435
615
|
http = Net::HTTP.new(auth_url.host, auth_url.port)
|
436
616
|
http.use_ssl = auth_url.scheme == "https"
|
437
617
|
|
438
|
-
request = Net::HTTP::Post.new(
|
618
|
+
request = Net::HTTP::Post.new(introspect_path)
|
439
619
|
request["content-type"] = json_response_content_type
|
440
620
|
request["accept"] = json_response_content_type
|
441
621
|
request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token })
|
@@ -491,7 +671,7 @@ module Rodauth
|
|
491
671
|
end
|
492
672
|
|
493
673
|
def oauth_unique_id_generator
|
494
|
-
SecureRandom.
|
674
|
+
SecureRandom.urlsafe_base64(32)
|
495
675
|
end
|
496
676
|
|
497
677
|
def generate_token_hash(token)
|
@@ -503,89 +683,106 @@ module Rodauth
|
|
503
683
|
end
|
504
684
|
|
505
685
|
unless method_defined?(:password_hash)
|
506
|
-
# :nocov:
|
507
686
|
# From login_requirements_base feature
|
508
|
-
if ENV["RACK_ENV"] == "test"
|
509
|
-
def password_hash_cost
|
510
|
-
BCrypt::Engine::MIN_COST
|
511
|
-
end
|
512
|
-
else
|
513
|
-
def password_hash_cost
|
514
|
-
BCrypt::Engine::DEFAULT_COST
|
515
|
-
end
|
516
|
-
end
|
517
687
|
|
518
688
|
def password_hash(password)
|
519
|
-
BCrypt::Password.create(password, cost:
|
689
|
+
BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
|
520
690
|
end
|
521
|
-
# :nocov:
|
522
691
|
end
|
523
692
|
|
524
693
|
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
525
694
|
create_params = {
|
526
|
-
oauth_grants_expires_in_column =>
|
695
|
+
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
527
696
|
}.merge(params)
|
528
697
|
|
529
|
-
|
530
|
-
|
698
|
+
rescue_from_uniqueness_error do
|
699
|
+
token = oauth_unique_id_generator
|
531
700
|
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
701
|
+
if oauth_tokens_token_hash_column
|
702
|
+
create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
703
|
+
else
|
704
|
+
create_params[oauth_tokens_token_column] = token
|
705
|
+
end
|
537
706
|
|
538
|
-
|
539
|
-
|
707
|
+
refresh_token = nil
|
708
|
+
if should_generate_refresh_token
|
709
|
+
refresh_token = oauth_unique_id_generator
|
540
710
|
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
711
|
+
if oauth_tokens_refresh_token_hash_column
|
712
|
+
create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
|
713
|
+
else
|
714
|
+
create_params[oauth_tokens_refresh_token_column] = refresh_token
|
715
|
+
end
|
545
716
|
end
|
717
|
+
oauth_token = _generate_oauth_token(create_params)
|
718
|
+
oauth_token[oauth_tokens_token_column] = token
|
719
|
+
oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
|
720
|
+
oauth_token
|
546
721
|
end
|
547
|
-
oauth_token = _generate_oauth_token(create_params)
|
548
|
-
|
549
|
-
oauth_token[oauth_tokens_token_column] = token
|
550
|
-
oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
|
551
|
-
oauth_token
|
552
722
|
end
|
553
723
|
|
554
724
|
def _generate_oauth_token(params = {})
|
555
725
|
ds = db[oauth_tokens_table]
|
556
726
|
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
727
|
+
if __one_oauth_token_per_account
|
728
|
+
|
729
|
+
token = __insert_or_update_and_return__(
|
730
|
+
ds,
|
731
|
+
oauth_tokens_id_column,
|
732
|
+
oauth_tokens_unique_columns,
|
733
|
+
params,
|
734
|
+
Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
|
735
|
+
([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
|
736
|
+
)
|
737
|
+
|
738
|
+
# if the previous operation didn't return a row, it means that the conditions
|
739
|
+
# invalidated the update, and the existing token is still valid.
|
740
|
+
token || ds.where(
|
741
|
+
oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
|
742
|
+
oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
|
743
|
+
).first
|
744
|
+
else
|
745
|
+
if oauth_reuse_access_token
|
746
|
+
unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
|
747
|
+
valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
|
748
|
+
.where(unique_conds).first
|
749
|
+
return valid_token if valid_token
|
563
750
|
end
|
564
|
-
|
565
|
-
retry
|
751
|
+
__insert_and_return__(ds, oauth_tokens_id_column, params)
|
566
752
|
end
|
567
753
|
end
|
568
754
|
|
569
|
-
def oauth_token_by_token(token
|
755
|
+
def oauth_token_by_token(token)
|
756
|
+
ds = db[oauth_tokens_table]
|
757
|
+
|
570
758
|
ds = if oauth_tokens_token_hash_column
|
571
|
-
|
759
|
+
ds.where(oauth_tokens_token_hash_column => generate_token_hash(token))
|
572
760
|
else
|
573
|
-
|
761
|
+
ds.where(oauth_tokens_token_column => token)
|
574
762
|
end
|
575
763
|
|
576
764
|
ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
577
765
|
.where(oauth_tokens_revoked_at_column => nil).first
|
578
766
|
end
|
579
767
|
|
580
|
-
def oauth_token_by_refresh_token(token,
|
768
|
+
def oauth_token_by_refresh_token(token, revoked: false)
|
769
|
+
ds = db[oauth_tokens_table]
|
770
|
+
#
|
771
|
+
# filter expired refresh tokens out.
|
772
|
+
# an expired refresh token is a token whose access token expired for a period longer than the
|
773
|
+
# refresh token expiration period.
|
774
|
+
#
|
775
|
+
ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
|
776
|
+
|
581
777
|
ds = if oauth_tokens_refresh_token_hash_column
|
582
|
-
|
778
|
+
ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
|
583
779
|
else
|
584
|
-
|
780
|
+
ds.where(oauth_tokens_refresh_token_column => token)
|
585
781
|
end
|
586
782
|
|
587
|
-
ds.where(
|
588
|
-
|
783
|
+
ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
|
784
|
+
|
785
|
+
ds.first
|
589
786
|
end
|
590
787
|
|
591
788
|
def json_access_token_payload(oauth_token)
|
@@ -654,7 +851,6 @@ module Rodauth
|
|
654
851
|
# set client ID/secret pairs
|
655
852
|
|
656
853
|
create_params.merge! \
|
657
|
-
oauth_applications_client_id_column => oauth_unique_id_generator,
|
658
854
|
oauth_applications_client_secret_column => \
|
659
855
|
secret_hash(oauth_application_params[oauth_application_client_secret_param])
|
660
856
|
|
@@ -664,29 +860,14 @@ module Rodauth
|
|
664
860
|
oauth_application_default_scope
|
665
861
|
end
|
666
862
|
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
false
|
671
|
-
rescue Sequel::ConstraintViolation => e
|
672
|
-
e
|
673
|
-
end
|
674
|
-
|
675
|
-
if raised
|
676
|
-
field = raised.message[/\.(.*)$/, 1]
|
677
|
-
case raised
|
678
|
-
when Sequel::UniqueConstraintViolation
|
679
|
-
throw_error(field, unique_error_message)
|
680
|
-
when Sequel::NotNullConstraintViolation
|
681
|
-
throw_error(field, null_error_message)
|
682
|
-
end
|
863
|
+
rescue_from_uniqueness_error do
|
864
|
+
create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
|
865
|
+
db[oauth_applications_table].insert(create_params)
|
683
866
|
end
|
684
|
-
|
685
|
-
!raised && id
|
686
867
|
end
|
687
868
|
|
688
869
|
# Authorize
|
689
|
-
def
|
870
|
+
def require_authorizable_account
|
690
871
|
require_account
|
691
872
|
end
|
692
873
|
|
@@ -699,6 +880,9 @@ module Rodauth
|
|
699
880
|
end
|
700
881
|
redirect_response_error("invalid_scope") unless check_valid_scopes?
|
701
882
|
|
883
|
+
if (response_mode = param_or_nil("response_mode")) && response_mode != "form_post"
|
884
|
+
redirect_response_error("invalid_request")
|
885
|
+
end
|
702
886
|
validate_pkce_challenge_params if use_oauth_pkce?
|
703
887
|
end
|
704
888
|
|
@@ -716,57 +900,79 @@ module Rodauth
|
|
716
900
|
).count.zero?
|
717
901
|
|
718
902
|
# if there's a previous oauth grant for the params combo, it means that this user has approved before.
|
719
|
-
|
720
903
|
request.env["REQUEST_METHOD"] = "POST"
|
721
904
|
end
|
722
905
|
|
723
|
-
def create_oauth_grant
|
724
|
-
create_params
|
906
|
+
def create_oauth_grant(create_params = {})
|
907
|
+
create_params.merge!(
|
725
908
|
oauth_grants_account_id_column => account_id,
|
726
909
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
727
910
|
oauth_grants_redirect_uri_column => redirect_uri,
|
728
|
-
oauth_grants_expires_in_column =>
|
911
|
+
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
|
729
912
|
oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
|
730
|
-
|
913
|
+
)
|
731
914
|
|
732
915
|
# Access Type flow
|
733
|
-
if use_oauth_access_type?
|
734
|
-
|
735
|
-
create_params[oauth_grants_access_type_column] = access_type
|
736
|
-
end
|
916
|
+
if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
|
917
|
+
create_params[oauth_grants_access_type_column] = access_type
|
737
918
|
end
|
738
919
|
|
739
920
|
# PKCE flow
|
740
|
-
if use_oauth_pkce?
|
921
|
+
if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
|
922
|
+
code_challenge_method = param_or_nil("code_challenge_method")
|
741
923
|
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
create_params[oauth_grants_code_challenge_column] = code_challenge
|
746
|
-
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
747
|
-
elsif oauth_require_pkce
|
748
|
-
redirect_response_error("code_challenge_required")
|
749
|
-
end
|
924
|
+
create_params[oauth_grants_code_challenge_column] = code_challenge
|
925
|
+
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
750
926
|
end
|
751
927
|
|
752
928
|
ds = db[oauth_grants_table]
|
753
929
|
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
930
|
+
rescue_from_uniqueness_error do
|
931
|
+
create_params[oauth_grants_code_column] = oauth_unique_id_generator
|
932
|
+
__insert_and_return__(ds, oauth_grants_id_column, create_params)
|
933
|
+
end
|
934
|
+
create_params[oauth_grants_code_column]
|
935
|
+
end
|
936
|
+
|
937
|
+
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
|
938
|
+
case param("response_type")
|
939
|
+
when "token"
|
940
|
+
redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
|
941
|
+
|
942
|
+
response_mode ||= "fragment"
|
943
|
+
response_params.replace(_do_authorize_token)
|
944
|
+
when "code"
|
945
|
+
response_mode ||= "query"
|
946
|
+
response_params.replace(_do_authorize_code)
|
947
|
+
when "none"
|
948
|
+
response_mode ||= "none"
|
949
|
+
when "", nil
|
950
|
+
response_mode ||= oauth_response_mode
|
951
|
+
response_params.replace(_do_authorize_code)
|
761
952
|
end
|
953
|
+
|
954
|
+
response_params["state"] = param("state") if param_or_nil("state")
|
955
|
+
|
956
|
+
[response_params, response_mode]
|
762
957
|
end
|
763
958
|
|
764
|
-
|
959
|
+
def _do_authorize_code
|
960
|
+
{ "code" => create_oauth_grant }
|
961
|
+
end
|
765
962
|
|
766
|
-
def
|
767
|
-
|
963
|
+
def _do_authorize_token
|
964
|
+
create_params = {
|
965
|
+
oauth_tokens_account_id_column => account_id,
|
966
|
+
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
967
|
+
oauth_tokens_scopes_column => scopes
|
968
|
+
}
|
969
|
+
oauth_token = generate_oauth_token(create_params, false)
|
970
|
+
|
971
|
+
json_access_token_payload(oauth_token)
|
768
972
|
end
|
769
973
|
|
974
|
+
# Access Tokens
|
975
|
+
|
770
976
|
def validate_oauth_token_params
|
771
977
|
unless (grant_type = param_or_nil("grant_type"))
|
772
978
|
redirect_response_error("invalid_request")
|
@@ -786,27 +992,56 @@ module Rodauth
|
|
786
992
|
def create_oauth_token
|
787
993
|
case param("grant_type")
|
788
994
|
when "authorization_code"
|
789
|
-
|
995
|
+
# fetch oauth grant
|
996
|
+
oauth_grant = db[oauth_grants_table].where(
|
997
|
+
oauth_grants_code_column => param("code"),
|
998
|
+
oauth_grants_redirect_uri_column => param("redirect_uri"),
|
999
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
1000
|
+
oauth_grants_revoked_at_column => nil
|
1001
|
+
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
1002
|
+
.for_update
|
1003
|
+
.first
|
1004
|
+
|
1005
|
+
redirect_response_error("invalid_grant") unless oauth_grant
|
1006
|
+
|
1007
|
+
create_params = {
|
1008
|
+
oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
|
1009
|
+
oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
|
1010
|
+
oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
|
1011
|
+
oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
|
1012
|
+
}
|
1013
|
+
create_oauth_token_from_authorization_code(oauth_grant, create_params)
|
790
1014
|
when "refresh_token"
|
791
|
-
|
792
|
-
|
793
|
-
|
1015
|
+
# fetch potentially revoked oauth token
|
1016
|
+
oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
|
1017
|
+
|
1018
|
+
if !oauth_token
|
1019
|
+
redirect_response_error("invalid_grant")
|
1020
|
+
elsif oauth_token[oauth_tokens_revoked_at_column]
|
1021
|
+
if oauth_refresh_token_protection_policy == "rotation"
|
1022
|
+
# https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
|
1023
|
+
#
|
1024
|
+
# If a refresh token is compromised and subsequently used by both the attacker and the legitimate
|
1025
|
+
# client, one of them will present an invalidated refresh token, which will inform the authorization
|
1026
|
+
# server of the breach. The authorization server cannot determine which party submitted the invalid
|
1027
|
+
# refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
|
1028
|
+
# forcing the legitimate client to obtain a fresh authorization grant.
|
1029
|
+
|
1030
|
+
db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
|
1031
|
+
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
1032
|
+
end
|
1033
|
+
redirect_response_error("invalid_grant")
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
update_params = {
|
1037
|
+
oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
|
1038
|
+
oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
1039
|
+
}
|
1040
|
+
create_oauth_token_from_token(oauth_token, update_params)
|
794
1041
|
end
|
795
1042
|
end
|
796
1043
|
|
797
|
-
def create_oauth_token_from_authorization_code(
|
798
|
-
# fetch oauth grant
|
799
|
-
oauth_grant = db[oauth_grants_table].where(
|
800
|
-
oauth_grants_code_column => param("code"),
|
801
|
-
oauth_grants_redirect_uri_column => param("redirect_uri"),
|
802
|
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
803
|
-
oauth_grants_revoked_at_column => nil
|
804
|
-
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
805
|
-
.for_update
|
806
|
-
.first
|
807
|
-
|
808
|
-
redirect_response_error("invalid_grant") unless oauth_grant
|
809
|
-
|
1044
|
+
def create_oauth_token_from_authorization_code(oauth_grant, create_params)
|
810
1045
|
# PKCE
|
811
1046
|
if use_oauth_pkce?
|
812
1047
|
if oauth_grant[oauth_grants_code_challenge_column]
|
@@ -818,13 +1053,6 @@ module Rodauth
|
|
818
1053
|
end
|
819
1054
|
end
|
820
1055
|
|
821
|
-
create_params = {
|
822
|
-
oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
|
823
|
-
oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
|
824
|
-
oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
|
825
|
-
oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
|
826
|
-
}
|
827
|
-
|
828
1056
|
# revoke oauth grant
|
829
1057
|
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
|
830
1058
|
.update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
@@ -835,40 +1063,41 @@ module Rodauth
|
|
835
1063
|
generate_oauth_token(create_params, should_generate_refresh_token)
|
836
1064
|
end
|
837
1065
|
|
838
|
-
def create_oauth_token_from_token(
|
839
|
-
|
840
|
-
oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
|
1066
|
+
def create_oauth_token_from_token(oauth_token, update_params)
|
1067
|
+
redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
|
841
1068
|
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
update_params = {
|
847
|
-
oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
|
848
|
-
oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
|
849
|
-
}
|
1069
|
+
rescue_from_uniqueness_error do
|
1070
|
+
oauth_tokens_ds = db[oauth_tokens_table]
|
1071
|
+
token = oauth_unique_id_generator
|
850
1072
|
|
851
|
-
|
852
|
-
|
853
|
-
else
|
854
|
-
update_params[oauth_tokens_token_column] = token
|
855
|
-
end
|
856
|
-
|
857
|
-
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
858
|
-
|
859
|
-
oauth_token = begin
|
860
|
-
if ds.supports_returning?(:update)
|
861
|
-
ds.returning.update(update_params).first
|
1073
|
+
if oauth_tokens_token_hash_column
|
1074
|
+
update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
862
1075
|
else
|
863
|
-
|
864
|
-
ds.first
|
1076
|
+
update_params[oauth_tokens_token_column] = token
|
865
1077
|
end
|
866
|
-
rescue Sequel::UniqueConstraintViolation
|
867
|
-
retry
|
868
|
-
end
|
869
1078
|
|
870
|
-
|
871
|
-
|
1079
|
+
oauth_token = if oauth_refresh_token_protection_policy == "rotation"
|
1080
|
+
insert_params = {
|
1081
|
+
**update_params,
|
1082
|
+
oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
|
1083
|
+
oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
|
1084
|
+
}
|
1085
|
+
|
1086
|
+
# revoke the refresh token
|
1087
|
+
oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
1088
|
+
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
1089
|
+
|
1090
|
+
insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column]
|
1091
|
+
__insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params)
|
1092
|
+
else
|
1093
|
+
# includes none
|
1094
|
+
ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
1095
|
+
__update_and_return__(ds, update_params)
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
oauth_token[oauth_tokens_token_column] = token
|
1099
|
+
oauth_token
|
1100
|
+
end
|
872
1101
|
end
|
873
1102
|
|
874
1103
|
TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
|
@@ -877,8 +1106,8 @@ module Rodauth
|
|
877
1106
|
|
878
1107
|
def validate_oauth_introspect_params
|
879
1108
|
# check if valid token hint type
|
880
|
-
if token_type_hint
|
881
|
-
redirect_response_error("unsupported_token_type")
|
1109
|
+
if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
1110
|
+
redirect_response_error("unsupported_token_type")
|
882
1111
|
end
|
883
1112
|
|
884
1113
|
redirect_response_error("invalid_request") unless param_or_nil("token")
|
@@ -896,48 +1125,35 @@ module Rodauth
|
|
896
1125
|
}
|
897
1126
|
end
|
898
1127
|
|
899
|
-
def before_introspect; end
|
900
|
-
|
901
1128
|
# Token revocation
|
902
1129
|
|
903
|
-
def before_revoke
|
904
|
-
require_oauth_application
|
905
|
-
end
|
906
|
-
|
907
1130
|
def validate_oauth_revoke_params
|
908
1131
|
# check if valid token hint type
|
909
|
-
|
1132
|
+
if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
1133
|
+
redirect_response_error("unsupported_token_type")
|
1134
|
+
end
|
910
1135
|
|
911
1136
|
redirect_response_error("invalid_request") unless param_or_nil("token")
|
912
1137
|
end
|
913
1138
|
|
914
1139
|
def revoke_oauth_token
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
when "refresh_token"
|
1140
|
+
token = param("token")
|
1141
|
+
|
1142
|
+
oauth_token = if param("token_type_hint") == "refresh_token"
|
919
1143
|
oauth_token_by_refresh_token(token)
|
1144
|
+
else
|
1145
|
+
oauth_token_by_token(token)
|
920
1146
|
end
|
921
1147
|
|
922
1148
|
redirect_response_error("invalid_request") unless oauth_token
|
923
1149
|
|
924
|
-
|
925
|
-
redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
|
926
|
-
else
|
927
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
928
|
-
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
929
|
-
end
|
1150
|
+
redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
|
930
1151
|
|
931
1152
|
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
|
932
1153
|
|
933
1154
|
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
934
1155
|
|
935
|
-
oauth_token =
|
936
|
-
ds.returning.update(update_params).first
|
937
|
-
else
|
938
|
-
ds.update(update_params)
|
939
|
-
ds.first
|
940
|
-
end
|
1156
|
+
oauth_token = __update_and_return__(ds, update_params)
|
941
1157
|
|
942
1158
|
oauth_token[oauth_tokens_token_column] = token
|
943
1159
|
oauth_token
|
@@ -955,7 +1171,13 @@ module Rodauth
|
|
955
1171
|
|
956
1172
|
def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
|
957
1173
|
if accepts_json?
|
958
|
-
|
1174
|
+
status_code = if respond_to?(:"#{error_code}_response_status")
|
1175
|
+
send(:"#{error_code}_response_status")
|
1176
|
+
else
|
1177
|
+
invalid_oauth_response_status
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
throw_json_response_error(status_code, error_code)
|
959
1181
|
else
|
960
1182
|
redirect_url = URI.parse(redirect_url)
|
961
1183
|
query_params = []
|
@@ -977,9 +1199,17 @@ module Rodauth
|
|
977
1199
|
end
|
978
1200
|
end
|
979
1201
|
|
980
|
-
def json_response_success(body)
|
1202
|
+
def json_response_success(body, cache = false)
|
981
1203
|
response.status = 200
|
982
1204
|
response["Content-Type"] ||= json_response_content_type
|
1205
|
+
if cache
|
1206
|
+
# defaulting to 1-day for everyone, for now at least
|
1207
|
+
max_age = 60 * 60 * 24
|
1208
|
+
response["Cache-Control"] = "private, max-age=#{max_age}"
|
1209
|
+
else
|
1210
|
+
response["Cache-Control"] = "no-store"
|
1211
|
+
response["Pragma"] = "no-cache"
|
1212
|
+
end
|
983
1213
|
json_payload = _json_response_body(body)
|
984
1214
|
response.write(json_payload)
|
985
1215
|
request.halt
|
@@ -1002,7 +1232,6 @@ module Rodauth
|
|
1002
1232
|
end
|
1003
1233
|
|
1004
1234
|
unless method_defined?(:_json_response_body)
|
1005
|
-
# :nocov:
|
1006
1235
|
def _json_response_body(hash)
|
1007
1236
|
if request.respond_to?(:convert_to_json)
|
1008
1237
|
request.send(:convert_to_json, hash)
|
@@ -1010,7 +1239,6 @@ module Rodauth
|
|
1010
1239
|
JSON.dump(hash)
|
1011
1240
|
end
|
1012
1241
|
end
|
1013
|
-
# :nocov:
|
1014
1242
|
end
|
1015
1243
|
|
1016
1244
|
def authorization_required
|
@@ -1018,7 +1246,7 @@ module Rodauth
|
|
1018
1246
|
throw_json_response_error(authorization_required_error_status, "invalid_client")
|
1019
1247
|
else
|
1020
1248
|
set_redirect_error_flash(require_authorization_error_flash)
|
1021
|
-
redirect(
|
1249
|
+
redirect(authorize_path)
|
1022
1250
|
end
|
1023
1251
|
end
|
1024
1252
|
|
@@ -1098,10 +1326,10 @@ module Rodauth
|
|
1098
1326
|
|
1099
1327
|
def oauth_server_metadata_body(path)
|
1100
1328
|
issuer = base_url
|
1101
|
-
issuer += "/#{path}" if
|
1329
|
+
issuer += "/#{path}" if path
|
1102
1330
|
|
1103
1331
|
responses_supported = %w[code]
|
1104
|
-
response_modes_supported = %w[query]
|
1332
|
+
response_modes_supported = %w[query form_post]
|
1105
1333
|
grant_types_supported = %w[authorization_code]
|
1106
1334
|
|
1107
1335
|
if use_oauth_implicit_grant_type?
|
@@ -1109,11 +1337,12 @@ module Rodauth
|
|
1109
1337
|
response_modes_supported << "fragment"
|
1110
1338
|
grant_types_supported << "implicit"
|
1111
1339
|
end
|
1340
|
+
|
1112
1341
|
{
|
1113
1342
|
issuer: issuer,
|
1114
|
-
authorization_endpoint:
|
1115
|
-
token_endpoint:
|
1116
|
-
registration_endpoint:
|
1343
|
+
authorization_endpoint: authorize_url,
|
1344
|
+
token_endpoint: token_url,
|
1345
|
+
registration_endpoint: route_url(oauth_applications_path),
|
1117
1346
|
scopes_supported: oauth_application_scopes,
|
1118
1347
|
response_types_supported: responses_supported,
|
1119
1348
|
response_modes_supported: response_modes_supported,
|
@@ -1123,143 +1352,12 @@ module Rodauth
|
|
1123
1352
|
ui_locales_supported: oauth_metadata_ui_locales_supported,
|
1124
1353
|
op_policy_uri: oauth_metadata_op_policy_uri,
|
1125
1354
|
op_tos_uri: oauth_metadata_op_tos_uri,
|
1126
|
-
revocation_endpoint:
|
1355
|
+
revocation_endpoint: revoke_url,
|
1127
1356
|
revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
|
1128
|
-
introspection_endpoint:
|
1357
|
+
introspection_endpoint: introspect_url,
|
1129
1358
|
introspection_endpoint_auth_methods_supported: %w[client_secret_basic],
|
1130
1359
|
code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
|
1131
1360
|
}
|
1132
1361
|
end
|
1133
|
-
|
1134
|
-
# /oauth-token
|
1135
|
-
route(:oauth_token) do |r|
|
1136
|
-
before_token
|
1137
|
-
|
1138
|
-
r.post do
|
1139
|
-
catch_error do
|
1140
|
-
validate_oauth_token_params
|
1141
|
-
|
1142
|
-
oauth_token = nil
|
1143
|
-
transaction do
|
1144
|
-
oauth_token = create_oauth_token
|
1145
|
-
end
|
1146
|
-
|
1147
|
-
json_response_success(json_access_token_payload(oauth_token))
|
1148
|
-
end
|
1149
|
-
|
1150
|
-
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
1151
|
-
end
|
1152
|
-
end
|
1153
|
-
|
1154
|
-
# /oauth-introspect
|
1155
|
-
route(:oauth_introspect) do |r|
|
1156
|
-
before_introspect
|
1157
|
-
|
1158
|
-
r.post do
|
1159
|
-
catch_error do
|
1160
|
-
validate_oauth_introspect_params
|
1161
|
-
|
1162
|
-
oauth_token = case param("token_type_hint")
|
1163
|
-
when "access_token"
|
1164
|
-
oauth_token_by_token(param("token"))
|
1165
|
-
when "refresh_token"
|
1166
|
-
oauth_token_by_refresh_token(param("token"))
|
1167
|
-
else
|
1168
|
-
oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
|
1169
|
-
end
|
1170
|
-
|
1171
|
-
if oauth_application
|
1172
|
-
redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
|
1173
|
-
elsif oauth_token
|
1174
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
1175
|
-
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
1176
|
-
end
|
1177
|
-
|
1178
|
-
json_response_success(json_token_introspect_payload(oauth_token))
|
1179
|
-
end
|
1180
|
-
|
1181
|
-
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
1182
|
-
end
|
1183
|
-
end
|
1184
|
-
|
1185
|
-
# /oauth-revoke
|
1186
|
-
route(:oauth_revoke) do |r|
|
1187
|
-
before_revoke
|
1188
|
-
|
1189
|
-
# access-token
|
1190
|
-
r.post do
|
1191
|
-
catch_error do
|
1192
|
-
validate_oauth_revoke_params
|
1193
|
-
|
1194
|
-
oauth_token = nil
|
1195
|
-
transaction do
|
1196
|
-
oauth_token = revoke_oauth_token
|
1197
|
-
after_revoke
|
1198
|
-
end
|
1199
|
-
|
1200
|
-
if accepts_json?
|
1201
|
-
json_response_success \
|
1202
|
-
"token" => oauth_token[oauth_tokens_token_column],
|
1203
|
-
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
|
1204
|
-
"revoked_at" => oauth_token[oauth_tokens_revoked_at_column]
|
1205
|
-
else
|
1206
|
-
set_notice_flash revoke_oauth_token_notice_flash
|
1207
|
-
redirect request.referer || "/"
|
1208
|
-
end
|
1209
|
-
end
|
1210
|
-
|
1211
|
-
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
1212
|
-
end
|
1213
|
-
end
|
1214
|
-
|
1215
|
-
# /oauth-authorize
|
1216
|
-
route(:oauth_authorize) do |r|
|
1217
|
-
require_account
|
1218
|
-
validate_oauth_grant_params
|
1219
|
-
try_approval_prompt if use_oauth_access_type? && request.get?
|
1220
|
-
|
1221
|
-
before_authorize
|
1222
|
-
|
1223
|
-
r.get do
|
1224
|
-
authorize_view
|
1225
|
-
end
|
1226
|
-
|
1227
|
-
r.post do
|
1228
|
-
code = nil
|
1229
|
-
query_params = []
|
1230
|
-
fragment_params = []
|
1231
|
-
|
1232
|
-
transaction do
|
1233
|
-
case param("response_type")
|
1234
|
-
when "token"
|
1235
|
-
redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
|
1236
|
-
|
1237
|
-
create_params = {
|
1238
|
-
oauth_tokens_account_id_column => account_id,
|
1239
|
-
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
1240
|
-
oauth_tokens_scopes_column => scopes
|
1241
|
-
}
|
1242
|
-
oauth_token = generate_oauth_token(create_params, false)
|
1243
|
-
|
1244
|
-
token_payload = json_access_token_payload(oauth_token)
|
1245
|
-
fragment_params.replace(token_payload.map { |k, v| "#{k}=#{v}" })
|
1246
|
-
when "code", "", nil
|
1247
|
-
code = create_oauth_grant
|
1248
|
-
query_params << "code=#{code}"
|
1249
|
-
else
|
1250
|
-
redirect_response_error("invalid_request")
|
1251
|
-
end
|
1252
|
-
after_authorize
|
1253
|
-
end
|
1254
|
-
|
1255
|
-
redirect_url = URI.parse(redirect_uri)
|
1256
|
-
query_params << "state=#{state}" if state
|
1257
|
-
query_params << redirect_url.query if redirect_url.query
|
1258
|
-
redirect_url.query = query_params.join("&") unless query_params.empty?
|
1259
|
-
redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
|
1260
|
-
|
1261
|
-
redirect(redirect_url.to_s)
|
1262
|
-
end
|
1263
|
-
end
|
1264
1362
|
end
|
1265
1363
|
end
|