rodauth-oauth 0.0.4 → 0.3.0

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