rodauth-oauth 0.0.4 → 0.3.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.
@@ -1,11 +1,21 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require "time"
3
4
  require "base64"
5
+ require "securerandom"
6
+ require "net/http"
7
+
8
+ require "rodauth/oauth/ttl_store"
9
+ require "rodauth/oauth/database_extensions"
4
10
 
5
11
  module Rodauth
6
12
  Feature.define(:oauth) do
7
13
  # RUBY EXTENSIONS
8
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:
9
19
  module RegexpExtensions
10
20
  refine(Regexp) do
11
21
  def match?(*args)
@@ -14,6 +24,7 @@ module Rodauth
14
24
  end
15
25
  end
16
26
  using(RegexpExtensions)
27
+ # :nocov:
17
28
  end
18
29
 
19
30
  unless String.method_defined?(:delete_suffix!)
@@ -35,9 +46,10 @@ module Rodauth
35
46
 
36
47
  SCOPES = %w[profile.read].freeze
37
48
 
49
+ SERVER_METADATA = OAuth::TtlStore.new
50
+
38
51
  before "authorize"
39
52
  after "authorize"
40
- after "authorize_failure"
41
53
 
42
54
  before "token"
43
55
 
@@ -49,15 +61,13 @@ module Rodauth
49
61
  before "create_oauth_application"
50
62
  after "create_oauth_application"
51
63
 
52
- error_flash "OAuth Authorization invalid parameters", "oauth_grant_valid_parameters"
53
-
54
64
  error_flash "Please authorize to continue", "require_authorization"
55
65
  error_flash "There was an error registering your oauth application", "create_oauth_application"
56
66
  notice_flash "Your oauth application has been registered", "create_oauth_application"
57
67
 
58
68
  notice_flash "The oauth token has been revoked", "revoke_oauth_token"
59
69
 
60
- view "oauth_authorize", "Authorize", "authorize"
70
+ view "authorize", "Authorize", "authorize"
61
71
  view "oauth_applications", "Oauth Applications", "oauth_applications"
62
72
  view "oauth_application", "Oauth Application", "oauth_application"
63
73
  view "new_oauth_application", "New Oauth Application", "new_oauth_application"
@@ -65,8 +75,9 @@ module Rodauth
65
75
 
66
76
  auth_value_method :json_response_content_type, "application/json"
67
77
 
68
- auth_value_method :oauth_grant_expires_in, 60 * 5 # 60 minutes
78
+ auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
69
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
70
81
  auth_value_method :use_oauth_implicit_grant_type?, false
71
82
  auth_value_method :use_oauth_pkce?, true
72
83
  auth_value_method :use_oauth_access_type?, true
@@ -74,19 +85,9 @@ module Rodauth
74
85
  auth_value_method :oauth_require_pkce, false
75
86
  auth_value_method :oauth_pkce_challenge_method, "S256"
76
87
 
77
- auth_value_method :oauth_valid_uri_schemes, %w[http https]
88
+ auth_value_method :oauth_valid_uri_schemes, %w[https]
78
89
 
79
- # URL PARAMS
80
-
81
- # Authorize / token
82
- %w[
83
- grant_type code refresh_token client_id client_secret scope
84
- state redirect_uri scopes token_type_hint token
85
- access_type approval_prompt response_type
86
- code_challenge code_challenge_method code_verifier
87
- ].each do |param|
88
- auth_value_method :"#{param}_param", param
89
- end
90
+ auth_value_method :oauth_scope_separator, " "
90
91
 
91
92
  # Application
92
93
  APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri client_secret].freeze
@@ -94,7 +95,11 @@ module Rodauth
94
95
 
95
96
  (APPLICATION_REQUIRED_PARAMS + %w[client_id]).each do |param|
96
97
  auth_value_method :"oauth_application_#{param}_param", param
98
+ translatable_method :"#{param}_label", param.gsub("_", " ").capitalize
97
99
  end
100
+ button "Register", "oauth_application"
101
+ button "Authorize", "oauth_authorize"
102
+ button "Revoke", "oauth_token_revoke"
98
103
 
99
104
  # OAuth Token
100
105
  auth_value_method :oauth_tokens_path, "oauth-tokens"
@@ -113,6 +118,8 @@ module Rodauth
113
118
  auth_value_method :oauth_tokens_token_hash_column, nil
114
119
  auth_value_method :oauth_tokens_refresh_token_hash_column, nil
115
120
 
121
+ # Access Token reuse
122
+ auth_value_method :oauth_reuse_access_token, false
116
123
  # OAuth Grants
117
124
  auth_value_method :oauth_grants_table, :oauth_grants
118
125
  auth_value_method :oauth_grants_id_column, :id
@@ -127,6 +134,7 @@ module Rodauth
127
134
 
128
135
  auth_value_method :authorization_required_error_status, 401
129
136
  auth_value_method :invalid_oauth_response_status, 400
137
+ auth_value_method :already_in_use_response_status, 409
130
138
 
131
139
  # OAuth Applications
132
140
  auth_value_method :oauth_applications_path, "oauth-applications"
@@ -144,13 +152,13 @@ module Rodauth
144
152
  auth_value_method :"oauth_applications_#{column}_column", column
145
153
  end
146
154
 
155
+ # Feature options
147
156
  auth_value_method :oauth_application_default_scope, SCOPES.first
148
157
  auth_value_method :oauth_application_scopes, SCOPES
149
158
  auth_value_method :oauth_token_type, "bearer"
159
+ auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
150
160
 
151
- auth_value_method :invalid_request, "Request is missing a required parameter"
152
- auth_value_method :invalid_client, "Invalid client"
153
- auth_value_method :unauthorized_client, "Unauthorized client"
161
+ auth_value_method :invalid_client_message, "Invalid client"
154
162
  auth_value_method :invalid_grant_type_message, "Invalid grant type"
155
163
  auth_value_method :invalid_grant_message, "Invalid grant"
156
164
  auth_value_method :invalid_scope_message, "Invalid scope"
@@ -160,6 +168,8 @@ module Rodauth
160
168
 
161
169
  auth_value_method :unique_error_message, "is already in use"
162
170
  auth_value_method :null_error_message, "is not filled"
171
+ auth_value_method :already_in_use_message, "error generating unique token"
172
+ auth_value_method :already_in_use_error_code, "invalid_request"
163
173
 
164
174
  # PKCE
165
175
  auth_value_method :code_challenge_required_error_code, "invalid_request"
@@ -173,38 +183,214 @@ module Rodauth
173
183
  auth_value_method :oauth_metadata_op_policy_uri, nil
174
184
  auth_value_method :oauth_metadata_op_tos_uri, nil
175
185
 
186
+ # Resource Server params
187
+ # Only required to use if the plugin is to be used in a resource server
188
+ auth_value_method :is_authorization_server?, true
189
+
190
+ auth_value_method :oauth_unique_id_generation_retries, 3
191
+
176
192
  auth_value_methods(
177
193
  :fetch_access_token,
178
194
  :oauth_unique_id_generator,
179
195
  :secret_matches?,
180
- :secret_hash
196
+ :secret_hash,
197
+ :generate_token_hash,
198
+ :authorization_server_url,
199
+ :before_introspection_request,
200
+ :require_authorizable_account,
201
+ :oauth_tokens_unique_columns
181
202
  )
182
203
 
183
204
  auth_value_methods(:only_json?)
184
205
 
185
- redirect(:oauth_application) do |id|
186
- "/#{oauth_applications_path}/#{id}"
206
+ auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
207
+
208
+ # /token
209
+ route(:token) do |r|
210
+ next unless is_authorization_server?
211
+
212
+ before_token_route
213
+ require_oauth_application
214
+
215
+ r.post do
216
+ catch_error do
217
+ validate_oauth_token_params
218
+
219
+ oauth_token = nil
220
+ transaction do
221
+ before_token
222
+ oauth_token = create_oauth_token
223
+ end
224
+
225
+ json_response_success(json_access_token_payload(oauth_token))
226
+ end
227
+
228
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request")
229
+ end
187
230
  end
188
231
 
189
- redirect(:require_authorization) do
190
- if logged_in?
191
- oauth_authorize_path
192
- elsif respond_to?(:login_redirect)
193
- login_redirect
194
- else
195
- default_redirect
232
+ # /introspect
233
+ route(:introspect) do |r|
234
+ next unless is_authorization_server?
235
+
236
+ before_introspect_route
237
+
238
+ r.post do
239
+ catch_error do
240
+ validate_oauth_introspect_params
241
+
242
+ before_introspect
243
+ oauth_token = case param("token_type_hint")
244
+ when "access_token"
245
+ oauth_token_by_token(param("token"))
246
+ when "refresh_token"
247
+ oauth_token_by_refresh_token(param("token"))
248
+ else
249
+ oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
250
+ end
251
+
252
+ if oauth_application
253
+ redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
254
+ elsif oauth_token
255
+ @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
256
+ oauth_token[oauth_tokens_oauth_application_id_column]).first
257
+ end
258
+
259
+ json_response_success(json_token_introspect_payload(oauth_token))
260
+ end
261
+
262
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request")
196
263
  end
197
264
  end
198
265
 
199
- auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
266
+ # /revoke
267
+ route(:revoke) do |r|
268
+ next unless is_authorization_server?
269
+
270
+ before_revoke_route
271
+ require_oauth_application
272
+
273
+ r.post do
274
+ catch_error do
275
+ validate_oauth_revoke_params
276
+
277
+ oauth_token = nil
278
+ transaction do
279
+ before_revoke
280
+ oauth_token = revoke_oauth_token
281
+ after_revoke
282
+ end
283
+
284
+ if accepts_json?
285
+ json_response_success \
286
+ "token" => oauth_token[oauth_tokens_token_column],
287
+ "refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
288
+ "revoked_at" => convert_timestamp(oauth_token[oauth_tokens_revoked_at_column])
289
+ else
290
+ set_notice_flash revoke_oauth_token_notice_flash
291
+ redirect request.referer || "/"
292
+ end
293
+ end
294
+
295
+ redirect_response_error("invalid_request", request.referer || "/")
296
+ end
297
+ end
298
+
299
+ # /authorize
300
+ route(:authorize) do |r|
301
+ next unless is_authorization_server?
302
+
303
+ before_authorize_route
304
+ require_authorizable_account
305
+
306
+ validate_oauth_grant_params
307
+ try_approval_prompt if use_oauth_access_type? && request.get?
308
+
309
+ r.get do
310
+ authorize_view
311
+ end
312
+
313
+ r.post do
314
+ redirect_url = URI.parse(redirect_uri)
315
+
316
+ transaction do
317
+ before_authorize
318
+ do_authorize(redirect_url)
319
+ end
320
+ redirect(redirect_url.to_s)
321
+ end
322
+ end
323
+
324
+ def oauth_server_metadata(issuer = nil)
325
+ request.on(".well-known") do
326
+ request.on("oauth-authorization-server") do
327
+ request.get do
328
+ json_response_success(oauth_server_metadata_body(issuer), true)
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ # /oauth-applications routes
335
+ def oauth_applications
336
+ request.on(oauth_applications_path) do
337
+ require_account
338
+
339
+ request.get "new" do
340
+ new_oauth_application_view
341
+ end
342
+
343
+ request.on(oauth_applications_id_pattern) do |id|
344
+ oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
345
+ next unless oauth_application
346
+
347
+ scope.instance_variable_set(:@oauth_application, oauth_application)
348
+
349
+ request.is do
350
+ request.get do
351
+ oauth_application_view
352
+ end
353
+ end
354
+
355
+ request.on(oauth_tokens_path) do
356
+ oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
357
+ scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
358
+ request.get do
359
+ oauth_tokens_view
360
+ end
361
+ end
362
+ end
363
+
364
+ request.get do
365
+ scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table])
366
+ oauth_applications_view
367
+ end
368
+
369
+ request.post do
370
+ catch_error do
371
+ validate_oauth_application_params
372
+
373
+ transaction do
374
+ before_create_oauth_application
375
+ id = create_oauth_application
376
+ after_create_oauth_application
377
+ set_notice_flash create_oauth_application_notice_flash
378
+ redirect "#{request.path}/#{id}"
379
+ end
380
+ end
381
+ set_error_flash create_oauth_application_error_flash
382
+ new_oauth_application_view
383
+ end
384
+ end
385
+ end
200
386
 
201
387
  def check_csrf?
202
388
  case request.path
203
- when oauth_token_path, oauth_introspect_path
389
+ when token_path, introspect_path
204
390
  false
205
- when oauth_revoke_path
391
+ when revoke_path
206
392
  !json_request?
207
- when oauth_authorize_path, %r{/#{oauth_applications_path}}
393
+ when authorize_path, %r{/#{oauth_applications_path}}
208
394
  only_json? ? false : super
209
395
  else
210
396
  super
@@ -235,39 +421,32 @@ module Rodauth
235
421
  @scope = scope
236
422
  end
237
423
 
238
- def state
239
- param_or_nil(state_param)
240
- end
241
-
242
424
  def scopes
243
- (param_or_nil(scopes_param) || oauth_application_default_scope).split(" ")
244
- end
245
-
246
- def client_id
247
- param_or_nil(client_id_param)
248
- end
249
-
250
- def client_secret
251
- param_or_nil(client_secret_param)
425
+ scope = request.params["scope"]
426
+ case scope
427
+ when Array
428
+ scope
429
+ when String
430
+ scope.split(" ")
431
+ when nil
432
+ [oauth_application_default_scope]
433
+ end
252
434
  end
253
435
 
254
436
  def redirect_uri
255
- param_or_nil(redirect_uri_param) || oauth_application[oauth_applications_redirect_uri_column]
256
- end
437
+ param_or_nil("redirect_uri") || begin
438
+ return unless oauth_application
257
439
 
258
- def token_type_hint
259
- param_or_nil(token_type_hint_param) || "access_token"
260
- end
261
-
262
- def token
263
- param_or_nil(token_param)
440
+ redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
441
+ redirect_uris.size == 1 ? redirect_uris.first : nil
442
+ end
264
443
  end
265
444
 
266
445
  def oauth_application
267
446
  return @oauth_application if defined?(@oauth_application)
268
447
 
269
448
  @oauth_application = begin
270
- client_id = param(client_id_param)
449
+ client_id = param_or_nil("client_id")
271
450
 
272
451
  return unless client_id
273
452
 
@@ -284,6 +463,8 @@ module Rodauth
284
463
 
285
464
  return unless scheme.downcase == oauth_token_type
286
465
 
466
+ return if token.empty?
467
+
287
468
  token
288
469
  end
289
470
 
@@ -291,80 +472,137 @@ module Rodauth
291
472
  return @authorization_token if defined?(@authorization_token)
292
473
 
293
474
  # check if there is a token
475
+ bearer_token = fetch_access_token
476
+
477
+ return unless bearer_token
478
+
294
479
  # check if token has not expired
295
480
  # check if token has been revoked
296
- @authorization_token = oauth_token_by_token(fetch_access_token)
481
+ @authorization_token = oauth_token_by_token(bearer_token)
297
482
  end
298
483
 
299
484
  def require_oauth_authorization(*scopes)
300
- authorization_required unless authorization_token
485
+ token_scopes = if is_authorization_server?
486
+ authorization_required unless authorization_token
487
+
488
+ scopes << oauth_application_default_scope if scopes.empty?
301
489
 
302
- scopes << oauth_application_default_scope if scopes.empty?
490
+ authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
491
+ else
492
+ bearer_token = fetch_access_token
303
493
 
304
- token_scopes = authorization_token[oauth_tokens_scopes_column].split(",")
494
+ authorization_required unless bearer_token
495
+
496
+ scopes << oauth_application_default_scope if scopes.empty?
497
+
498
+ # where in resource server, NOT the authorization server.
499
+ payload = introspection_request("access_token", bearer_token)
500
+
501
+ authorization_required unless payload["active"]
502
+
503
+ payload["scope"].split(oauth_scope_separator)
504
+ end
305
505
 
306
506
  authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
307
507
  end
308
508
 
309
- # /oauth-applications routes
310
- def oauth_applications
311
- request.on(oauth_applications_path) do
312
- require_account
509
+ def post_configure
510
+ super
511
+ self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
313
512
 
314
- request.get "new" do
315
- new_oauth_application_view
513
+ # Check whether we can reutilize db entries for the same account / application pair
514
+ one_oauth_token_per_account = begin
515
+ db.indexes(oauth_tokens_table).values.any? do |definition|
516
+ definition[:unique] &&
517
+ definition[:columns] == oauth_tokens_unique_columns
316
518
  end
317
- request.on(oauth_applications_id_pattern) do |id|
318
- oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
319
- scope.instance_variable_set(:@oauth_application, oauth_application)
519
+ end
520
+ self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
521
+ end
320
522
 
321
- request.is do
322
- request.get do
323
- oauth_application_view
324
- end
325
- end
523
+ def use_date_arithmetic?
524
+ true
525
+ end
326
526
 
327
- request.on(oauth_tokens_path) do
328
- oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
329
- scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
330
- oauth_tokens_view
331
- end
332
- end
527
+ private
333
528
 
334
- request.get do
335
- scope.instance_variable_set(:@oauth_applications, db[:oauth_applications])
336
- oauth_applications_view
337
- end
529
+ def rescue_from_uniqueness_error(&block)
530
+ retries = oauth_unique_id_generation_retries
531
+ begin
532
+ transaction(savepoint: :only, &block)
533
+ rescue Sequel::UniqueConstraintViolation
534
+ redirect_response_error("already_in_use") if retries.zero?
535
+ retries -= 1
536
+ retry
537
+ end
538
+ end
338
539
 
339
- request.post do
340
- catch_error do
341
- validate_oauth_application_params
540
+ # OAuth Token Unique/Reuse
541
+ def oauth_tokens_unique_columns
542
+ [
543
+ oauth_tokens_oauth_application_id_column,
544
+ oauth_tokens_account_id_column,
545
+ oauth_tokens_scopes_column
546
+ ]
547
+ end
342
548
 
343
- transaction do
344
- before_create_oauth_application
345
- id = create_oauth_application
346
- after_create_oauth_application
347
- set_notice_flash create_oauth_application_notice_flash
348
- redirect oauth_application_redirect(id)
349
- end
350
- end
351
- set_error_flash create_oauth_application_error_flash
352
- new_oauth_application_view
353
- end
354
- end
549
+ def authorization_server_url
550
+ base_url
355
551
  end
356
552
 
357
- def oauth_server_metadata(issuer = nil)
358
- request.on(".well-known") do
359
- request.on("oauth-authorization-server") do
360
- request.get do
361
- json_response_success(oauth_server_metadata_body(issuer))
362
- end
363
- end
553
+ def authorization_server_metadata
554
+ auth_url = URI(authorization_server_url)
555
+
556
+ server_metadata = SERVER_METADATA[auth_url]
557
+
558
+ return server_metadata if server_metadata
559
+
560
+ SERVER_METADATA.set(auth_url) do
561
+ http = Net::HTTP.new(auth_url.host, auth_url.port)
562
+ http.use_ssl = auth_url.scheme == "https"
563
+
564
+ request = Net::HTTP::Get.new("/.well-known/oauth-authorization-server")
565
+ request["accept"] = json_response_content_type
566
+ response = http.request(request)
567
+ authorization_required unless response.code.to_i == 200
568
+
569
+ # time-to-live
570
+ ttl = if response.key?("cache-control")
571
+ cache_control = response["cache-control"]
572
+ cache_control[/max-age=(\d+)/, 1].to_i
573
+ elsif response.key?("expires")
574
+ Time.parse(response["expires"]).to_i - Time.now.to_i
575
+ end
576
+
577
+ [JSON.parse(response.body, symbolize_names: true), ttl]
364
578
  end
365
579
  end
366
580
 
367
- private
581
+ def introspection_request(token_type_hint, token)
582
+ auth_url = URI(authorization_server_url)
583
+ http = Net::HTTP.new(auth_url.host, auth_url.port)
584
+ http.use_ssl = auth_url.scheme == "https"
585
+
586
+ request = Net::HTTP::Post.new(introspect_path)
587
+ request["content-type"] = json_response_content_type
588
+ request["accept"] = json_response_content_type
589
+ request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token })
590
+
591
+ before_introspection_request(request)
592
+ response = http.request(request)
593
+ authorization_required unless response.code.to_i == 200
594
+
595
+ JSON.parse(response.body)
596
+ end
597
+
598
+ def before_introspection_request(request); end
599
+
600
+ def template_path(page)
601
+ path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
602
+ return super unless File.exist?(path)
603
+
604
+ path
605
+ end
368
606
 
369
607
  # to be used internally. Same semantics as require account, must:
370
608
  # fetch an authorization basic header
@@ -378,8 +616,8 @@ module Rodauth
378
616
  if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
379
617
  client_id, client_secret = Base64.decode64(token).split(/:/, 2)
380
618
  else
381
- client_id = param_or_nil(client_id_param)
382
- client_secret = param_or_nil(client_secret_param)
619
+ client_id = param_or_nil("client_id")
620
+ client_secret = param_or_nil("client_secret")
383
621
  end
384
622
 
385
623
  authorization_required unless client_id
@@ -387,7 +625,7 @@ module Rodauth
387
625
  @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
388
626
 
389
627
  # skip if using pkce
390
- return if @oauth_application && use_oauth_pkce? && param_or_nil(code_verifier_param)
628
+ return if @oauth_application && use_oauth_pkce? && param_or_nil("code_verifier")
391
629
 
392
630
  authorization_required unless @oauth_application && secret_matches?(@oauth_application, client_secret)
393
631
  end
@@ -401,7 +639,7 @@ module Rodauth
401
639
  end
402
640
 
403
641
  def oauth_unique_id_generator
404
- SecureRandom.hex(32)
642
+ SecureRandom.urlsafe_base64(32)
405
643
  end
406
644
 
407
645
  def generate_token_hash(token)
@@ -414,88 +652,105 @@ module Rodauth
414
652
 
415
653
  unless method_defined?(:password_hash)
416
654
  # From login_requirements_base feature
417
- if ENV["RACK_ENV"] == "test"
418
- def password_hash_cost
419
- BCrypt::Engine::MIN_COST
420
- end
421
- else
422
- # :nocov:
423
- def password_hash_cost
424
- BCrypt::Engine::DEFAULT_COST
425
- end
426
- # :nocov:
427
- end
428
655
 
429
656
  def password_hash(password)
430
- BCrypt::Password.create(password, cost: password_hash_cost)
657
+ BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
431
658
  end
432
659
  end
433
660
 
434
661
  def generate_oauth_token(params = {}, should_generate_refresh_token = true)
435
662
  create_params = {
436
- oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
663
+ oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
437
664
  }.merge(params)
438
665
 
439
- token = oauth_unique_id_generator
440
- refresh_token = nil
666
+ rescue_from_uniqueness_error do
667
+ token = oauth_unique_id_generator
441
668
 
442
- if oauth_tokens_token_hash_column
443
- create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
444
- else
445
- create_params[oauth_tokens_token_column] = token
446
- end
669
+ if oauth_tokens_token_hash_column
670
+ create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
671
+ else
672
+ create_params[oauth_tokens_token_column] = token
673
+ end
447
674
 
448
- if should_generate_refresh_token
449
- refresh_token = oauth_unique_id_generator
675
+ refresh_token = nil
676
+ if should_generate_refresh_token
677
+ refresh_token = oauth_unique_id_generator
450
678
 
451
- if oauth_tokens_refresh_token_hash_column
452
- create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
453
- else
454
- create_params[oauth_tokens_refresh_token_column] = refresh_token
679
+ if oauth_tokens_refresh_token_hash_column
680
+ create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
681
+ else
682
+ create_params[oauth_tokens_refresh_token_column] = refresh_token
683
+ end
455
684
  end
685
+ oauth_token = _generate_oauth_token(create_params)
686
+ oauth_token[oauth_tokens_token_column] = token
687
+ oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
688
+ oauth_token
456
689
  end
457
- oauth_token = _generate_oauth_token(create_params)
458
-
459
- oauth_token[oauth_tokens_token_column] = token
460
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
461
- oauth_token
462
690
  end
463
691
 
464
692
  def _generate_oauth_token(params = {})
465
693
  ds = db[oauth_tokens_table]
466
694
 
467
- begin
468
- if ds.supports_returning?(:insert)
469
- ds.returning.insert(params)
470
- else
471
- id = ds.insert(params)
472
- ds.where(oauth_tokens_id_column => id).first
695
+ if __one_oauth_token_per_account
696
+
697
+ token = __insert_or_update_and_return__(
698
+ ds,
699
+ oauth_tokens_id_column,
700
+ oauth_tokens_unique_columns,
701
+ params,
702
+ Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
703
+ ([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
704
+ )
705
+
706
+ # if the previous operation didn't return a row, it means that the conditions
707
+ # invalidated the update, and the existing token is still valid.
708
+ token || ds.where(
709
+ oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
710
+ oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
711
+ ).first
712
+ else
713
+ if oauth_reuse_access_token
714
+ unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
715
+ valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
716
+ .where(unique_conds).first
717
+ return valid_token if valid_token
473
718
  end
474
- rescue Sequel::UniqueConstraintViolation
475
- retry
719
+ __insert_and_return__(ds, oauth_tokens_id_column, params)
476
720
  end
477
721
  end
478
722
 
479
- def oauth_token_by_token(token, dataset = db[oauth_tokens_table])
723
+ def oauth_token_by_token(token)
724
+ ds = db[oauth_tokens_table]
725
+
480
726
  ds = if oauth_tokens_token_hash_column
481
- dataset.where(oauth_tokens_token_hash_column => generate_token_hash(token))
727
+ ds.where(oauth_tokens_token_hash_column => generate_token_hash(token))
482
728
  else
483
- dataset.where(oauth_tokens_token_column => token)
729
+ ds.where(oauth_tokens_token_column => token)
484
730
  end
485
731
 
486
732
  ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
487
733
  .where(oauth_tokens_revoked_at_column => nil).first
488
734
  end
489
735
 
490
- def oauth_token_by_refresh_token(token, dataset = db[oauth_tokens_table])
736
+ def oauth_token_by_refresh_token(token, revoked: false)
737
+ ds = db[oauth_tokens_table]
738
+ #
739
+ # filter expired refresh tokens out.
740
+ # an expired refresh token is a token whose access token expired for a period longer than the
741
+ # refresh token expiration period.
742
+ #
743
+ ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
744
+
491
745
  ds = if oauth_tokens_refresh_token_hash_column
492
- dataset.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
746
+ ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
493
747
  else
494
- dataset.where(oauth_tokens_refresh_token_column => token)
748
+ ds.where(oauth_tokens_refresh_token_column => token)
495
749
  end
496
750
 
497
- ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
498
- .where(oauth_tokens_revoked_at_column => nil).first
751
+ ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
752
+
753
+ ds.first
499
754
  end
500
755
 
501
756
  def json_access_token_payload(oauth_token)
@@ -523,11 +778,21 @@ module Rodauth
523
778
 
524
779
  def validate_oauth_application_params
525
780
  oauth_application_params.each do |key, value|
526
- if key == oauth_application_homepage_url_param ||
527
- key == oauth_application_redirect_uri_param
781
+ if key == oauth_application_homepage_url_param
782
+
783
+ set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
784
+
785
+ elsif key == oauth_application_redirect_uri_param
528
786
 
529
- set_field_error(key, invalid_url_message) unless URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(value)
787
+ if value.respond_to?(:each)
788
+ value.each do |uri|
789
+ next if uri.empty?
530
790
 
791
+ set_field_error(key, invalid_url_message) unless check_valid_uri?(uri)
792
+ end
793
+ else
794
+ set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
795
+ end
531
796
  elsif key == oauth_application_scopes_param
532
797
 
533
798
  value.each do |scope|
@@ -545,57 +810,38 @@ module Rodauth
545
810
  oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
546
811
  oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
547
812
  oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
548
- oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param],
549
- oauth_applications_redirect_uri_column => oauth_application_params[oauth_application_redirect_uri_param]
813
+ oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
550
814
  }
551
815
 
816
+ redirect_uris = oauth_application_params[oauth_application_redirect_uri_param]
817
+ redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each)
818
+ create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty?
552
819
  # set client ID/secret pairs
553
820
 
554
821
  create_params.merge! \
555
- oauth_applications_client_id_column => oauth_unique_id_generator,
556
822
  oauth_applications_client_secret_column => \
557
823
  secret_hash(oauth_application_params[oauth_application_client_secret_param])
558
824
 
559
825
  create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column]
560
- create_params[oauth_applications_scopes_column].join(",")
826
+ create_params[oauth_applications_scopes_column].join(oauth_scope_separator)
561
827
  else
562
828
  oauth_application_default_scope
563
829
  end
564
830
 
565
- ds = db[oauth_applications_table]
566
-
567
- id = nil
568
- raised = begin
569
- id = if ds.supports_returning?(:insert)
570
- ds.returning(oauth_applications_id_column).insert(create_params)
571
- else
572
- id = db[oauth_applications_table].insert(create_params)
573
- db[oauth_applications_table].where(oauth_applications_id_column => id).get(oauth_applications_id_column)
574
- end
575
- false
576
- rescue Sequel::ConstraintViolation => e
577
- e
578
- end
579
-
580
- if raised
581
- field = raised.message[/\.(.*)$/, 1]
582
- case raised
583
- when Sequel::UniqueConstraintViolation
584
- throw_error(field, unique_error_message)
585
- when Sequel::NotNullConstraintViolation
586
- throw_error(field, null_error_message)
587
- end
831
+ rescue_from_uniqueness_error do
832
+ create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
833
+ db[oauth_applications_table].insert(create_params)
588
834
  end
589
-
590
- !raised && id
591
835
  end
592
836
 
593
837
  # Authorize
594
- def before_authorize
838
+ def require_authorizable_account
595
839
  require_account
596
840
  end
597
841
 
598
842
  def validate_oauth_grant_params
843
+ redirect_response_error("invalid_request", request.referer || default_redirect) unless oauth_application && check_valid_redirect_uri?
844
+
599
845
  unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? &&
600
846
  check_valid_approval_prompt? && check_valid_response_type?
601
847
  redirect_response_error("invalid_request")
@@ -606,7 +852,7 @@ module Rodauth
606
852
  end
607
853
 
608
854
  def try_approval_prompt
609
- approval_prompt = param_or_nil(approval_prompt_param)
855
+ approval_prompt = param_or_nil("approval_prompt")
610
856
 
611
857
  return unless approval_prompt && approval_prompt == "auto"
612
858
 
@@ -614,109 +860,159 @@ module Rodauth
614
860
  oauth_grants_account_id_column => account_id,
615
861
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
616
862
  oauth_grants_redirect_uri_column => redirect_uri,
617
- oauth_grants_scopes_column => scopes.join(","),
863
+ oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
618
864
  oauth_grants_access_type_column => "online"
619
865
  ).count.zero?
620
866
 
621
867
  # if there's a previous oauth grant for the params combo, it means that this user has approved before.
622
-
623
868
  request.env["REQUEST_METHOD"] = "POST"
624
869
  end
625
870
 
626
- def create_oauth_grant
627
- create_params = {
871
+ def create_oauth_grant(create_params = {})
872
+ create_params.merge!(
628
873
  oauth_grants_account_id_column => account_id,
629
874
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
630
875
  oauth_grants_redirect_uri_column => redirect_uri,
631
- oauth_grants_code_column => oauth_unique_id_generator,
632
- oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
633
- oauth_grants_scopes_column => scopes.join(",")
634
- }
876
+ oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
877
+ oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
878
+ )
635
879
 
636
880
  # Access Type flow
637
- if use_oauth_access_type?
638
- if (access_type = param_or_nil(access_type_param))
639
- create_params[oauth_grants_access_type_column] = access_type
640
- end
881
+ if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
882
+ create_params[oauth_grants_access_type_column] = access_type
641
883
  end
642
884
 
643
885
  # PKCE flow
644
- if use_oauth_pkce?
886
+ if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
887
+ code_challenge_method = param_or_nil("code_challenge_method")
645
888
 
646
- if (code_challenge = param_or_nil(code_challenge_param))
647
- code_challenge_method = param_or_nil(code_challenge_method_param)
648
-
649
- create_params[oauth_grants_code_challenge_column] = code_challenge
650
- create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
651
- elsif oauth_require_pkce
652
- redirect_response_error("code_challenge_required")
653
- end
889
+ create_params[oauth_grants_code_challenge_column] = code_challenge
890
+ create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
654
891
  end
655
892
 
656
893
  ds = db[oauth_grants_table]
657
894
 
658
- begin
659
- if ds.supports_returning?(:insert)
660
- ds.returning(authorize_code_column).insert(create_params)
895
+ rescue_from_uniqueness_error do
896
+ create_params[oauth_grants_code_column] = oauth_unique_id_generator
897
+ __insert_and_return__(ds, oauth_grants_id_column, create_params)
898
+ end
899
+ create_params[oauth_grants_code_column]
900
+ end
901
+
902
+ def do_authorize(redirect_url, query_params = [], fragment_params = [])
903
+ case param("response_type")
904
+ when "token"
905
+ redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
906
+
907
+ fragment_params.replace(_do_authorize_token.map { |k, v| "#{k}=#{v}" })
908
+ when "code", "", nil
909
+ query_params.replace(_do_authorize_code.map { |k, v| "#{k}=#{v}" })
910
+ end
911
+
912
+ if param_or_nil("state")
913
+ if !fragment_params.empty?
914
+ fragment_params << "state=#{param('state')}"
661
915
  else
662
- id = ds.insert(create_params)
663
- ds.where(oauth_grants_id_column => id).get(oauth_grants_code_column)
916
+ query_params << "state=#{param('state')}"
664
917
  end
665
- rescue Sequel::UniqueConstraintViolation
666
- retry
667
918
  end
919
+
920
+ query_params << redirect_url.query if redirect_url.query
921
+
922
+ redirect_url.query = query_params.join("&") unless query_params.empty?
923
+ redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
668
924
  end
669
925
 
670
- # Access Tokens
926
+ def _do_authorize_code
927
+ { "code" => create_oauth_grant }
928
+ end
671
929
 
672
- def before_token
673
- require_oauth_application
930
+ def _do_authorize_token
931
+ create_params = {
932
+ oauth_tokens_account_id_column => account_id,
933
+ oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
934
+ oauth_tokens_scopes_column => scopes
935
+ }
936
+ oauth_token = generate_oauth_token(create_params, false)
937
+
938
+ json_access_token_payload(oauth_token)
674
939
  end
675
940
 
941
+ # Access Tokens
942
+
676
943
  def validate_oauth_token_params
677
- unless (grant_type = param_or_nil(grant_type_param))
944
+ unless (grant_type = param_or_nil("grant_type"))
678
945
  redirect_response_error("invalid_request")
679
946
  end
680
947
 
681
948
  case grant_type
682
949
  when "authorization_code"
683
- redirect_response_error("invalid_request") unless param_or_nil(code_param)
950
+ redirect_response_error("invalid_request") unless param_or_nil("code")
684
951
 
685
952
  when "refresh_token"
686
- redirect_response_error("invalid_request") unless param_or_nil(refresh_token_param)
953
+ redirect_response_error("invalid_request") unless param_or_nil("refresh_token")
687
954
  else
688
955
  redirect_response_error("invalid_request")
689
956
  end
690
957
  end
691
958
 
692
959
  def create_oauth_token
693
- case param(grant_type_param)
960
+ case param("grant_type")
694
961
  when "authorization_code"
695
- create_oauth_token_from_authorization_code(oauth_application)
962
+ # fetch oauth grant
963
+ oauth_grant = db[oauth_grants_table].where(
964
+ oauth_grants_code_column => param("code"),
965
+ oauth_grants_redirect_uri_column => param("redirect_uri"),
966
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
967
+ oauth_grants_revoked_at_column => nil
968
+ ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
969
+ .for_update
970
+ .first
971
+
972
+ redirect_response_error("invalid_grant") unless oauth_grant
973
+
974
+ create_params = {
975
+ oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
976
+ oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
977
+ oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
978
+ oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
979
+ }
980
+ create_oauth_token_from_authorization_code(oauth_grant, create_params)
696
981
  when "refresh_token"
697
- create_oauth_token_from_token(oauth_application)
698
- else
699
- redirect_response_error("invalid_grant")
982
+ # fetch potentially revoked oauth token
983
+ oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
984
+
985
+ if !oauth_token
986
+ redirect_response_error("invalid_grant")
987
+ elsif oauth_token[oauth_tokens_revoked_at_column]
988
+ if oauth_refresh_token_protection_policy == "rotation"
989
+ # https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
990
+ #
991
+ # If a refresh token is compromised and subsequently used by both the attacker and the legitimate
992
+ # client, one of them will present an invalidated refresh token, which will inform the authorization
993
+ # server of the breach. The authorization server cannot determine which party submitted the invalid
994
+ # refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
995
+ # forcing the legitimate client to obtain a fresh authorization grant.
996
+
997
+ db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
998
+ .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
999
+ end
1000
+ redirect_response_error("invalid_grant")
1001
+ end
1002
+
1003
+ update_params = {
1004
+ oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
1005
+ oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
1006
+ }
1007
+ create_oauth_token_from_token(oauth_token, update_params)
700
1008
  end
701
1009
  end
702
1010
 
703
- def create_oauth_token_from_authorization_code(oauth_application)
704
- # fetch oauth grant
705
- oauth_grant = db[oauth_grants_table].where(
706
- oauth_grants_code_column => param(code_param),
707
- oauth_grants_redirect_uri_column => param(redirect_uri_param),
708
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
709
- oauth_grants_revoked_at_column => nil
710
- ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
711
- .for_update
712
- .first
713
-
714
- redirect_response_error("invalid_grant") unless oauth_grant
715
-
1011
+ def create_oauth_token_from_authorization_code(oauth_grant, create_params)
716
1012
  # PKCE
717
1013
  if use_oauth_pkce?
718
1014
  if oauth_grant[oauth_grants_code_challenge_column]
719
- code_verifier = param_or_nil(code_verifier_param)
1015
+ code_verifier = param_or_nil("code_verifier")
720
1016
 
721
1017
  redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
722
1018
  elsif oauth_require_pkce
@@ -724,13 +1020,6 @@ module Rodauth
724
1020
  end
725
1021
  end
726
1022
 
727
- create_params = {
728
- oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
729
- oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
730
- oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
731
- oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
732
- }
733
-
734
1023
  # revoke oauth grant
735
1024
  db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
736
1025
  .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
@@ -741,40 +1030,41 @@ module Rodauth
741
1030
  generate_oauth_token(create_params, should_generate_refresh_token)
742
1031
  end
743
1032
 
744
- def create_oauth_token_from_token(oauth_application)
745
- # fetch oauth token
746
- oauth_token = oauth_token_by_refresh_token(param(refresh_token_param))
1033
+ def create_oauth_token_from_token(oauth_token, update_params)
1034
+ redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
747
1035
 
748
- redirect_response_error("invalid_grant") unless oauth_token && token_from_application?(oauth_token, oauth_application)
749
-
750
- token = oauth_unique_id_generator
751
-
752
- update_params = {
753
- oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
754
- oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
755
- }
1036
+ rescue_from_uniqueness_error do
1037
+ oauth_tokens_ds = db[oauth_tokens_table]
1038
+ token = oauth_unique_id_generator
756
1039
 
757
- if oauth_tokens_token_hash_column
758
- update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
759
- else
760
- update_params[oauth_tokens_token_column] = token
761
- end
762
-
763
- ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
764
-
765
- oauth_token = begin
766
- if ds.supports_returning?(:update)
767
- ds.returning.update(update_params)
1040
+ if oauth_tokens_token_hash_column
1041
+ update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
768
1042
  else
769
- ds.update(update_params)
770
- ds.first
1043
+ update_params[oauth_tokens_token_column] = token
771
1044
  end
772
- rescue Sequel::UniqueConstraintViolation
773
- retry
774
- end
775
1045
 
776
- oauth_token[oauth_tokens_token_column] = token
777
- oauth_token
1046
+ oauth_token = if oauth_refresh_token_protection_policy == "rotation"
1047
+ insert_params = {
1048
+ **update_params,
1049
+ oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
1050
+ oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
1051
+ }
1052
+
1053
+ # revoke the refresh token
1054
+ oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
1055
+ .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
1056
+
1057
+ insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column]
1058
+ __insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params)
1059
+ else
1060
+ # includes none
1061
+ ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
1062
+ __update_and_return__(ds, update_params)
1063
+ end
1064
+
1065
+ oauth_token[oauth_tokens_token_column] = token
1066
+ oauth_token
1067
+ end
778
1068
  end
779
1069
 
780
1070
  TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
@@ -783,11 +1073,11 @@ module Rodauth
783
1073
 
784
1074
  def validate_oauth_introspect_params
785
1075
  # check if valid token hint type
786
- if token_type_hint
787
- redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(token_type_hint)
1076
+ if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1077
+ redirect_response_error("unsupported_token_type")
788
1078
  end
789
1079
 
790
- redirect_response_error("invalid_request") unless param_or_nil(token_param)
1080
+ redirect_response_error("invalid_request") unless param_or_nil("token")
791
1081
  end
792
1082
 
793
1083
  def json_token_introspect_payload(token)
@@ -795,50 +1085,42 @@ module Rodauth
795
1085
 
796
1086
  {
797
1087
  active: true,
798
- scope: token[oauth_tokens_scopes_column].gsub(",", " "),
1088
+ scope: token[oauth_tokens_scopes_column],
799
1089
  client_id: oauth_application[oauth_applications_client_id_column],
800
1090
  # username
801
1091
  token_type: oauth_token_type
802
1092
  }
803
1093
  end
804
1094
 
805
- def before_introspect
806
- require_oauth_application
807
- end
808
-
809
1095
  # Token revocation
810
1096
 
811
- def before_revoke
812
- require_oauth_application
813
- end
814
-
815
1097
  def validate_oauth_revoke_params
816
1098
  # check if valid token hint type
817
- redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(token_type_hint)
1099
+ if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1100
+ redirect_response_error("unsupported_token_type")
1101
+ end
818
1102
 
819
- redirect_response_error("invalid_request") unless param_or_nil(token_param)
1103
+ redirect_response_error("invalid_request") unless param_or_nil("token")
820
1104
  end
821
1105
 
822
1106
  def revoke_oauth_token
823
- oauth_token = case token_type_hint
824
- when "access_token"
825
- oauth_token_by_token(token)
826
- when "refresh_token"
1107
+ token = param("token")
1108
+
1109
+ oauth_token = if param("token_type_hint") == "refresh_token"
827
1110
  oauth_token_by_refresh_token(token)
1111
+ else
1112
+ oauth_token_by_token(token)
828
1113
  end
829
1114
 
830
- redirect_response_error("invalid_request") unless oauth_token && token_from_application?(oauth_token, oauth_application)
1115
+ redirect_response_error("invalid_request") unless oauth_token
1116
+
1117
+ redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
831
1118
 
832
1119
  update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
833
1120
 
834
1121
  ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
835
1122
 
836
- oauth_token = if ds.supports_returning?(:update)
837
- ds.returning.update(update_params)
838
- else
839
- ds.update(update_params)
840
- ds.first
841
- end
1123
+ oauth_token = __update_and_return__(ds, update_params)
842
1124
 
843
1125
  oauth_token[oauth_tokens_token_column] = token
844
1126
  oauth_token
@@ -854,9 +1136,15 @@ module Rodauth
854
1136
 
855
1137
  # Response helpers
856
1138
 
857
- def redirect_response_error(error_code, redirect_url = request.referer || default_redirect)
1139
+ def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
858
1140
  if accepts_json?
859
- throw_json_response_error(invalid_oauth_response_status, error_code)
1141
+ status_code = if respond_to?(:"#{error_code}_response_status")
1142
+ send(:"#{error_code}_response_status")
1143
+ else
1144
+ invalid_oauth_response_status
1145
+ end
1146
+
1147
+ throw_json_response_error(status_code, error_code)
860
1148
  else
861
1149
  redirect_url = URI.parse(redirect_url)
862
1150
  query_params = []
@@ -878,9 +1166,17 @@ module Rodauth
878
1166
  end
879
1167
  end
880
1168
 
881
- def json_response_success(body)
1169
+ def json_response_success(body, cache = false)
882
1170
  response.status = 200
883
1171
  response["Content-Type"] ||= json_response_content_type
1172
+ if cache
1173
+ # defaulting to 1-day for everyone, for now at least
1174
+ max_age = 60 * 60 * 24
1175
+ response["Cache-Control"] = "private, max-age=#{max_age}"
1176
+ else
1177
+ response["Cache-Control"] = "no-store"
1178
+ response["Pragma"] = "no-cache"
1179
+ end
884
1180
  json_payload = _json_response_body(body)
885
1181
  response.write(json_payload)
886
1182
  request.halt
@@ -917,18 +1213,22 @@ module Rodauth
917
1213
  throw_json_response_error(authorization_required_error_status, "invalid_client")
918
1214
  else
919
1215
  set_redirect_error_flash(require_authorization_error_flash)
920
- redirect(require_authorization_redirect)
1216
+ redirect(authorize_path)
921
1217
  end
922
1218
  end
923
1219
 
1220
+ def check_valid_uri?(uri)
1221
+ URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
1222
+ end
1223
+
924
1224
  def check_valid_scopes?
925
1225
  return false unless scopes
926
1226
 
927
- (scopes - oauth_application[oauth_applications_scopes_column].split(",")).empty?
1227
+ (scopes - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
928
1228
  end
929
1229
 
930
1230
  def check_valid_redirect_uri?
931
- redirect_uri == oauth_application[oauth_applications_redirect_uri_column]
1231
+ oauth_application[oauth_applications_redirect_uri_column].split(" ").include?(redirect_uri)
932
1232
  end
933
1233
 
934
1234
  ACCESS_TYPES = %w[offline online].freeze
@@ -936,7 +1236,7 @@ module Rodauth
936
1236
  def check_valid_access_type?
937
1237
  return true unless use_oauth_access_type?
938
1238
 
939
- access_type = param_or_nil(access_type_param)
1239
+ access_type = param_or_nil("access_type")
940
1240
  !access_type || ACCESS_TYPES.include?(access_type)
941
1241
  end
942
1242
 
@@ -945,12 +1245,12 @@ module Rodauth
945
1245
  def check_valid_approval_prompt?
946
1246
  return true unless use_oauth_access_type?
947
1247
 
948
- approval_prompt = param_or_nil(approval_prompt_param)
1248
+ approval_prompt = param_or_nil("approval_prompt")
949
1249
  !approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
950
1250
  end
951
1251
 
952
1252
  def check_valid_response_type?
953
- response_type = param_or_nil(response_type_param)
1253
+ response_type = param_or_nil("response_type")
954
1254
 
955
1255
  return true if response_type.nil? || response_type == "code"
956
1256
 
@@ -962,9 +1262,9 @@ module Rodauth
962
1262
  # PKCE
963
1263
 
964
1264
  def validate_pkce_challenge_params
965
- if param_or_nil(code_challenge_param)
1265
+ if param_or_nil("code_challenge")
966
1266
 
967
- challenge_method = param_or_nil(code_challenge_method_param)
1267
+ challenge_method = param_or_nil("code_challenge_method")
968
1268
  redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
969
1269
  else
970
1270
  return unless oauth_require_pkce
@@ -993,7 +1293,7 @@ module Rodauth
993
1293
 
994
1294
  def oauth_server_metadata_body(path)
995
1295
  issuer = base_url
996
- issuer += "/#{path}" if issuer
1296
+ issuer += "/#{path}" if path
997
1297
 
998
1298
  responses_supported = %w[code]
999
1299
  response_modes_supported = %w[query]
@@ -1004,11 +1304,12 @@ module Rodauth
1004
1304
  response_modes_supported << "fragment"
1005
1305
  grant_types_supported << "implicit"
1006
1306
  end
1307
+
1007
1308
  {
1008
1309
  issuer: issuer,
1009
- authorization_endpoint: oauth_authorize_url,
1010
- token_endpoint: oauth_token_url,
1011
- registration_endpoint: "#{base_url}/#{oauth_applications_path}",
1310
+ authorization_endpoint: authorize_url,
1311
+ token_endpoint: token_url,
1312
+ registration_endpoint: route_url(oauth_applications_path),
1012
1313
  scopes_supported: oauth_application_scopes,
1013
1314
  response_types_supported: responses_supported,
1014
1315
  response_modes_supported: response_modes_supported,
@@ -1018,138 +1319,12 @@ module Rodauth
1018
1319
  ui_locales_supported: oauth_metadata_ui_locales_supported,
1019
1320
  op_policy_uri: oauth_metadata_op_policy_uri,
1020
1321
  op_tos_uri: oauth_metadata_op_tos_uri,
1021
- revocation_endpoint: oauth_revoke_url,
1322
+ revocation_endpoint: revoke_url,
1022
1323
  revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
1023
- introspection_endpoint: oauth_introspect_url,
1324
+ introspection_endpoint: introspect_url,
1024
1325
  introspection_endpoint_auth_methods_supported: %w[client_secret_basic],
1025
1326
  code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
1026
1327
  }
1027
1328
  end
1028
-
1029
- # /oauth-token
1030
- route(:oauth_token) do |r|
1031
- before_token
1032
-
1033
- r.post do
1034
- catch_error do
1035
- validate_oauth_token_params
1036
-
1037
- oauth_token = nil
1038
- transaction do
1039
- oauth_token = create_oauth_token
1040
- end
1041
-
1042
- json_response_success(json_access_token_payload(oauth_token))
1043
- end
1044
-
1045
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
1046
- end
1047
- end
1048
-
1049
- # /oauth-introspect
1050
- route(:oauth_introspect) do |r|
1051
- before_introspect
1052
-
1053
- r.post do
1054
- catch_error do
1055
- validate_oauth_introspect_params
1056
-
1057
- oauth_token = case param(token_type_hint_param)
1058
- when "access_token"
1059
- oauth_token_by_token(param(token_param))
1060
- when "refresh_token"
1061
- oauth_token_by_refresh_token(param(token_param))
1062
- else
1063
- oauth_token_by_token(param(token_param)) || oauth_token_by_refresh_token(param(token_param))
1064
- end
1065
-
1066
- redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
1067
-
1068
- json_response_success(json_token_introspect_payload(oauth_token))
1069
- end
1070
-
1071
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
1072
- end
1073
- end
1074
-
1075
- # /oauth-revoke
1076
- route(:oauth_revoke) do |r|
1077
- before_revoke
1078
-
1079
- # access-token
1080
- r.post do
1081
- catch_error do
1082
- validate_oauth_revoke_params
1083
-
1084
- oauth_token = nil
1085
- transaction do
1086
- oauth_token = revoke_oauth_token
1087
- after_revoke
1088
- end
1089
-
1090
- if accepts_json?
1091
- json_response_success \
1092
- "token" => oauth_token[oauth_tokens_token_column],
1093
- "refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
1094
- "revoked_at" => oauth_token[oauth_tokens_revoked_at_column]
1095
- else
1096
- set_notice_flash revoke_oauth_token_notice_flash
1097
- redirect request.referer || "/"
1098
- end
1099
- end
1100
-
1101
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
1102
- end
1103
- end
1104
-
1105
- # /oauth-authorize
1106
- route(:oauth_authorize) do |r|
1107
- require_account
1108
- validate_oauth_grant_params
1109
- try_approval_prompt if use_oauth_access_type? && request.get?
1110
-
1111
- before_authorize
1112
-
1113
- r.get do
1114
- authorize_view
1115
- end
1116
-
1117
- r.post do
1118
- code = nil
1119
- query_params = []
1120
- fragment_params = []
1121
-
1122
- transaction do
1123
- case param(response_type_param)
1124
- when "token"
1125
- redirect_response_error("invalid_request", redirect_uri) unless use_oauth_implicit_grant_type?
1126
-
1127
- create_params = {
1128
- oauth_tokens_account_id_column => account_id,
1129
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
1130
- oauth_tokens_scopes_column => scopes
1131
- }
1132
- oauth_token = generate_oauth_token(create_params, false)
1133
-
1134
- token_payload = json_access_token_payload(oauth_token)
1135
- fragment_params.replace(token_payload.map { |k, v| "#{k}=#{v}" })
1136
- when "code", "", nil
1137
- code = create_oauth_grant
1138
- query_params << "code=#{code}"
1139
- else
1140
- redirect_response_error("invalid_request")
1141
- end
1142
- after_authorize
1143
- end
1144
-
1145
- redirect_url = URI.parse(redirect_uri)
1146
- query_params << "state=#{state}" if state
1147
- query_params << redirect_url.query if redirect_url.query
1148
- redirect_url.query = query_params.join("&") unless query_params.empty?
1149
- redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
1150
-
1151
- redirect(redirect_url.to_s)
1152
- end
1153
- end
1154
1329
  end
1155
1330
  end