rodauth-oauth 0.7.4 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -424
  3. data/README.md +30 -390
  4. data/doc/release_notes/0_0_1.md +3 -0
  5. data/doc/release_notes/0_0_2.md +15 -0
  6. data/doc/release_notes/0_0_3.md +31 -0
  7. data/doc/release_notes/0_0_4.md +36 -0
  8. data/doc/release_notes/0_0_5.md +36 -0
  9. data/doc/release_notes/0_0_6.md +21 -0
  10. data/doc/release_notes/0_1_0.md +44 -0
  11. data/doc/release_notes/0_2_0.md +43 -0
  12. data/doc/release_notes/0_3_0.md +28 -0
  13. data/doc/release_notes/0_4_0.md +18 -0
  14. data/doc/release_notes/0_4_1.md +9 -0
  15. data/doc/release_notes/0_4_2.md +5 -0
  16. data/doc/release_notes/0_4_3.md +3 -0
  17. data/doc/release_notes/0_5_0.md +11 -0
  18. data/doc/release_notes/0_5_1.md +13 -0
  19. data/doc/release_notes/0_6_0.md +9 -0
  20. data/doc/release_notes/0_6_1.md +6 -0
  21. data/doc/release_notes/0_7_0.md +20 -0
  22. data/doc/release_notes/0_7_1.md +10 -0
  23. data/doc/release_notes/0_7_2.md +21 -0
  24. data/doc/release_notes/0_7_3.md +10 -0
  25. data/doc/release_notes/0_7_4.md +5 -0
  26. data/doc/release_notes/0_8_0.md +37 -0
  27. data/doc/release_notes/0_9_0.md +56 -0
  28. data/doc/release_notes/0_9_1.md +9 -0
  29. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +25 -4
  30. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +11 -0
  31. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +20 -0
  32. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +27 -10
  33. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +17 -5
  34. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +39 -0
  35. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +6 -5
  36. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +12 -15
  37. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +21 -1
  38. data/lib/rodauth/features/oauth.rb +3 -1418
  39. data/lib/rodauth/features/oauth_application_management.rb +225 -0
  40. data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
  41. data/lib/rodauth/features/oauth_authorization_code_grant.rb +252 -0
  42. data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
  43. data/lib/rodauth/features/oauth_base.rb +778 -0
  44. data/lib/rodauth/features/oauth_client_credentials_grant.rb +33 -0
  45. data/lib/rodauth/features/oauth_device_grant.rb +220 -0
  46. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +252 -0
  47. data/lib/rodauth/features/oauth_http_mac.rb +3 -21
  48. data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
  49. data/lib/rodauth/features/oauth_jwt.rb +275 -100
  50. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
  51. data/lib/rodauth/features/oauth_management_base.rb +68 -0
  52. data/lib/rodauth/features/oauth_pkce.rb +98 -0
  53. data/lib/rodauth/features/oauth_resource_server.rb +21 -0
  54. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
  55. data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
  56. data/lib/rodauth/features/oauth_token_management.rb +79 -0
  57. data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
  58. data/lib/rodauth/features/oidc.rb +38 -9
  59. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +147 -0
  60. data/lib/rodauth/oauth/database_extensions.rb +15 -2
  61. data/lib/rodauth/oauth/jwe_extensions.rb +64 -0
  62. data/lib/rodauth/oauth/refinements.rb +48 -0
  63. data/lib/rodauth/oauth/ttl_store.rb +9 -3
  64. data/lib/rodauth/oauth/version.rb +1 -1
  65. data/locales/en.yml +33 -12
  66. data/templates/authorize.str +57 -8
  67. data/templates/client_secret_field.str +2 -2
  68. data/templates/description_field.str +1 -1
  69. data/templates/device_search.str +11 -0
  70. data/templates/device_verification.str +24 -0
  71. data/templates/homepage_url_field.str +2 -2
  72. data/templates/jwks_field.str +4 -0
  73. data/templates/jwt_public_key_field.str +4 -0
  74. data/templates/name_field.str +1 -1
  75. data/templates/new_oauth_application.str +9 -0
  76. data/templates/oauth_application.str +7 -3
  77. data/templates/oauth_application_oauth_tokens.str +52 -0
  78. data/templates/oauth_applications.str +3 -2
  79. data/templates/oauth_tokens.str +10 -11
  80. data/templates/redirect_uri_field.str +2 -2
  81. metadata +80 -3
  82. data/lib/rodauth/features/oauth_saml.rb +0 -104
@@ -0,0 +1,778 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "base64"
5
+ require "securerandom"
6
+ require "net/http"
7
+ require "rodauth/version"
8
+ require "rodauth/oauth/version"
9
+ require "rodauth/oauth/ttl_store"
10
+ require "rodauth/oauth/database_extensions"
11
+ require "rodauth/oauth/refinements"
12
+
13
+ module Rodauth
14
+ Feature.define(:oauth_base, :OauthBase) do
15
+ using RegexpExtensions
16
+
17
+ SCOPES = %w[profile.read].freeze
18
+
19
+ before "token"
20
+
21
+ error_flash "Please authorize to continue", "require_authorization"
22
+ error_flash "You are not authorized to revoke this token", "revoke_unauthorized_account"
23
+
24
+ button "Cancel", "oauth_cancel"
25
+
26
+ auth_value_method :json_response_content_type, "application/json"
27
+
28
+ auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
29
+ auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
30
+ auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
31
+ auth_value_method :oauth_unique_id_generation_retries, 3
32
+
33
+ auth_value_method :oauth_response_mode, "query"
34
+ auth_value_method :oauth_auth_methods_supported, %w[client_secret_basic client_secret_post]
35
+
36
+ auth_value_method :oauth_scope_separator, " "
37
+
38
+ auth_value_method :oauth_tokens_table, :oauth_tokens
39
+ auth_value_method :oauth_tokens_id_column, :id
40
+
41
+ %i[
42
+ oauth_application_id oauth_token_id oauth_grant_id account_id
43
+ token refresh_token scopes
44
+ expires_in revoked_at
45
+ ].each do |column|
46
+ auth_value_method :"oauth_tokens_#{column}_column", column
47
+ end
48
+
49
+ # Oauth Token Hash
50
+ auth_value_method :oauth_tokens_token_hash_column, nil
51
+ auth_value_method :oauth_tokens_refresh_token_hash_column, nil
52
+
53
+ # Access Token reuse
54
+ auth_value_method :oauth_reuse_access_token, false
55
+
56
+ auth_value_method :oauth_applications_table, :oauth_applications
57
+ auth_value_method :oauth_applications_id_column, :id
58
+
59
+ %i[
60
+ account_id
61
+ name description scopes
62
+ client_id client_secret
63
+ homepage_url redirect_uri
64
+ token_endpoint_auth_method grant_types response_types
65
+ logo_uri tos_uri policy_uri jwks jwks_uri
66
+ contacts software_id software_version
67
+ ].each do |column|
68
+ auth_value_method :"oauth_applications_#{column}_column", column
69
+ end
70
+
71
+ auth_value_method :authorization_required_error_status, 401
72
+ auth_value_method :invalid_oauth_response_status, 400
73
+ auth_value_method :already_in_use_response_status, 409
74
+
75
+ # Feature options
76
+ auth_value_method :oauth_application_default_scope, SCOPES.first
77
+ auth_value_method :oauth_application_scopes, SCOPES
78
+ auth_value_method :oauth_token_type, "bearer"
79
+ auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
80
+
81
+ translatable_method :invalid_client_message, "Invalid client"
82
+ translatable_method :invalid_grant_type_message, "Invalid grant type"
83
+ translatable_method :invalid_grant_message, "Invalid grant"
84
+ translatable_method :invalid_scope_message, "Invalid scope"
85
+ translatable_method :unsupported_token_type_message, "Invalid token type hint"
86
+
87
+ translatable_method :unique_error_message, "is already in use"
88
+ translatable_method :already_in_use_message, "error generating unique token"
89
+ auth_value_method :already_in_use_error_code, "invalid_request"
90
+ auth_value_method :invalid_grant_type_error_code, "unsupported_grant_type"
91
+
92
+ # Resource Server params
93
+ # Only required to use if the plugin is to be used in a resource server
94
+ auth_value_method :is_authorization_server?, true
95
+
96
+ auth_value_methods(:only_json?)
97
+
98
+ auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
99
+
100
+ # METADATA
101
+ auth_value_method :oauth_metadata_service_documentation, nil
102
+ auth_value_method :oauth_metadata_ui_locales_supported, nil
103
+ auth_value_method :oauth_metadata_op_policy_uri, nil
104
+ auth_value_method :oauth_metadata_op_tos_uri, nil
105
+
106
+ auth_value_methods(
107
+ :fetch_access_token,
108
+ :secret_hash,
109
+ :generate_token_hash,
110
+ :secret_matches?,
111
+ :authorization_server_url,
112
+ :oauth_unique_id_generator,
113
+ :oauth_tokens_unique_columns,
114
+ :require_authorizable_account
115
+ )
116
+
117
+ # /token
118
+ route(:token) do |r|
119
+ next unless is_authorization_server?
120
+
121
+ before_token_route
122
+ require_oauth_application
123
+
124
+ r.post do
125
+ catch_error do
126
+ validate_oauth_token_params
127
+
128
+ oauth_token = nil
129
+
130
+ transaction do
131
+ before_token
132
+ oauth_token = create_oauth_token(param("grant_type"))
133
+ end
134
+
135
+ json_response_success(json_access_token_payload(oauth_token))
136
+ end
137
+
138
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request")
139
+ end
140
+ end
141
+
142
+ def oauth_server_metadata(issuer = nil)
143
+ request.on(".well-known") do
144
+ request.on("oauth-authorization-server") do
145
+ request.get do
146
+ json_response_success(oauth_server_metadata_body(issuer), true)
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ def check_csrf?
153
+ case request.path
154
+ when token_path
155
+ false
156
+ else
157
+ super
158
+ end
159
+ end
160
+
161
+ # Overrides session_value, so that a valid authorization token also authenticates a request
162
+ def session_value
163
+ super || begin
164
+ return unless authorization_token
165
+
166
+ authorization_token[oauth_tokens_account_id_column]
167
+ end
168
+ end
169
+
170
+ def accepts_json?
171
+ return true if only_json?
172
+
173
+ (accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
174
+ end
175
+
176
+ unless method_defined?(:json_request?)
177
+ # copied from the jwt feature
178
+ def json_request?
179
+ return @json_request if defined?(@json_request)
180
+
181
+ @json_request = request.content_type =~ json_request_regexp
182
+ end
183
+ end
184
+
185
+ def scopes
186
+ scope = request.params["scope"]
187
+ case scope
188
+ when Array
189
+ scope
190
+ when String
191
+ scope.split(" ")
192
+ when nil
193
+ Array(oauth_application_default_scope)
194
+ end
195
+ end
196
+
197
+ def redirect_uri
198
+ param_or_nil("redirect_uri") || begin
199
+ return unless oauth_application
200
+
201
+ redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
202
+ redirect_uris.size == 1 ? redirect_uris.first : nil
203
+ end
204
+ end
205
+
206
+ def oauth_application
207
+ return @oauth_application if defined?(@oauth_application)
208
+
209
+ @oauth_application = begin
210
+ client_id = param_or_nil("client_id")
211
+
212
+ return unless client_id
213
+
214
+ db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first
215
+ end
216
+ end
217
+
218
+ def fetch_access_token
219
+ value = request.env["HTTP_AUTHORIZATION"]
220
+
221
+ return unless value && !value.empty?
222
+
223
+ scheme, token = value.split(" ", 2)
224
+
225
+ return unless scheme.downcase == oauth_token_type
226
+
227
+ return if token.nil? || token.empty?
228
+
229
+ token
230
+ end
231
+
232
+ def authorization_token
233
+ return @authorization_token if defined?(@authorization_token)
234
+
235
+ # check if there is a token
236
+ bearer_token = fetch_access_token
237
+
238
+ return unless bearer_token
239
+
240
+ @authorization_token = if is_authorization_server?
241
+ # check if token has not expired
242
+ # check if token has been revoked
243
+ oauth_token_by_token(bearer_token)
244
+ else
245
+ # where in resource server, NOT the authorization server.
246
+ payload = introspection_request("access_token", bearer_token)
247
+
248
+ return unless payload["active"]
249
+
250
+ payload
251
+ end
252
+ end
253
+
254
+ def require_oauth_authorization(*scopes)
255
+ authorization_required unless authorization_token
256
+
257
+ scopes << oauth_application_default_scope if scopes.empty?
258
+
259
+ token_scopes = if is_authorization_server?
260
+ authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
261
+ else
262
+ aux_scopes = authorization_token["scope"]
263
+ if aux_scopes
264
+ aux_scopes.split(oauth_scope_separator)
265
+ else
266
+ []
267
+ end
268
+ end
269
+
270
+ authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
271
+ end
272
+
273
+ def use_date_arithmetic?
274
+ true
275
+ end
276
+
277
+ def post_configure
278
+ super
279
+
280
+ # all of the extensions below involve DB changes. Resource server mode doesn't use
281
+ # database functions for OAuth though.
282
+ return unless is_authorization_server?
283
+
284
+ self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
285
+
286
+ # Check whether we can reutilize db entries for the same account / application pair
287
+ one_oauth_token_per_account = db.indexes(oauth_tokens_table).values.any? do |definition|
288
+ definition[:unique] &&
289
+ definition[:columns] == oauth_tokens_unique_columns
290
+ end
291
+
292
+ self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
293
+
294
+ i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n)
295
+ end
296
+
297
+ private
298
+
299
+ def require_authorizable_account
300
+ require_account
301
+ end
302
+
303
+ def rescue_from_uniqueness_error(&block)
304
+ retries = oauth_unique_id_generation_retries
305
+ begin
306
+ transaction(savepoint: :only, &block)
307
+ rescue Sequel::UniqueConstraintViolation
308
+ redirect_response_error("already_in_use") if retries.zero?
309
+ retries -= 1
310
+ retry
311
+ end
312
+ end
313
+
314
+ # OAuth Token Unique/Reuse
315
+ def oauth_tokens_unique_columns
316
+ [
317
+ oauth_tokens_oauth_application_id_column,
318
+ oauth_tokens_account_id_column,
319
+ oauth_tokens_scopes_column
320
+ ]
321
+ end
322
+
323
+ def authorization_server_url
324
+ base_url
325
+ end
326
+
327
+ def template_path(page)
328
+ path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
329
+ return super unless File.exist?(path)
330
+
331
+ path
332
+ end
333
+
334
+ # to be used internally. Same semantics as require account, must:
335
+ # fetch an authorization basic header
336
+ # parse client id and secret
337
+ #
338
+ def require_oauth_application
339
+ # get client credentials
340
+ auth_method = nil
341
+ client_id = client_secret = nil
342
+
343
+ if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
344
+ # client_secret_basic
345
+ client_id, client_secret = Base64.decode64(token).split(/:/, 2)
346
+ auth_method = "client_secret_basic"
347
+ else
348
+ # client_secret_post
349
+ client_id = param_or_nil("client_id")
350
+ client_secret = param_or_nil("client_secret")
351
+ auth_method = "client_secret_post" if client_secret
352
+ end
353
+
354
+ authorization_required unless client_id
355
+
356
+ @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
357
+
358
+ authorization_required unless @oauth_application
359
+
360
+ authorization_required unless authorized_oauth_application?(@oauth_application, client_secret, auth_method)
361
+ end
362
+
363
+ def authorized_oauth_application?(oauth_application, client_secret, auth_method)
364
+ supported_auth_methods = if oauth_application[oauth_applications_token_endpoint_auth_method_column]
365
+ oauth_application[oauth_applications_token_endpoint_auth_method_column].split(/ +/)
366
+ else
367
+ oauth_auth_methods_supported
368
+ end
369
+
370
+ if auth_method
371
+ supported_auth_methods.include?(auth_method) && secret_matches?(oauth_application, client_secret)
372
+ else
373
+ supported_auth_methods.include?("none")
374
+ end
375
+ end
376
+
377
+ def no_auth_oauth_application?(_oauth_application)
378
+ supported_auth_methods.include?("none")
379
+ end
380
+
381
+ def require_oauth_application_from_account
382
+ ds = db[oauth_applications_table]
383
+ .join(oauth_tokens_table, Sequel[oauth_tokens_table][oauth_tokens_oauth_application_id_column] =>
384
+ Sequel[oauth_applications_table][oauth_applications_id_column])
385
+ .where(oauth_token_by_token_ds(param("token")).opts.fetch(:where, true))
386
+ .where(Sequel[oauth_applications_table][oauth_applications_account_id_column] => account_id)
387
+
388
+ @oauth_application = ds.qualify.first
389
+ return if @oauth_application
390
+
391
+ set_redirect_error_flash revoke_unauthorized_account_error_flash
392
+ redirect request.referer || "/"
393
+ end
394
+
395
+ def secret_matches?(oauth_application, secret)
396
+ BCrypt::Password.new(oauth_application[oauth_applications_client_secret_column]) == secret
397
+ end
398
+
399
+ def secret_hash(secret)
400
+ password_hash(secret)
401
+ end
402
+
403
+ def oauth_unique_id_generator
404
+ SecureRandom.urlsafe_base64(32)
405
+ end
406
+
407
+ def generate_token_hash(token)
408
+ Base64.urlsafe_encode64(Digest::SHA256.digest(token))
409
+ end
410
+
411
+ def token_from_application?(oauth_token, oauth_application)
412
+ oauth_token[oauth_tokens_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
413
+ end
414
+
415
+ unless method_defined?(:password_hash)
416
+ # From login_requirements_base feature
417
+
418
+ def password_hash(password)
419
+ BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
420
+ end
421
+ end
422
+
423
+ def generate_oauth_token(params = {}, should_generate_refresh_token = true)
424
+ create_params = {
425
+ oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
426
+ }.merge(params)
427
+
428
+ rescue_from_uniqueness_error do
429
+ token = oauth_unique_id_generator
430
+
431
+ if oauth_tokens_token_hash_column
432
+ create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
433
+ else
434
+ create_params[oauth_tokens_token_column] = token
435
+ end
436
+
437
+ refresh_token = nil
438
+ if should_generate_refresh_token
439
+ refresh_token = oauth_unique_id_generator
440
+
441
+ if oauth_tokens_refresh_token_hash_column
442
+ create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
443
+ else
444
+ create_params[oauth_tokens_refresh_token_column] = refresh_token
445
+ end
446
+ end
447
+ oauth_token = _generate_oauth_token(create_params)
448
+ oauth_token[oauth_tokens_token_column] = token
449
+ oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
450
+ oauth_token
451
+ end
452
+ end
453
+
454
+ def _generate_oauth_token(params = {})
455
+ ds = db[oauth_tokens_table]
456
+
457
+ if __one_oauth_token_per_account
458
+
459
+ token = __insert_or_update_and_return__(
460
+ ds,
461
+ oauth_tokens_id_column,
462
+ oauth_tokens_unique_columns,
463
+ params,
464
+ Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
465
+ ([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
466
+ )
467
+
468
+ # if the previous operation didn't return a row, it means that the conditions
469
+ # invalidated the update, and the existing token is still valid.
470
+ token || ds.where(
471
+ oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
472
+ oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
473
+ ).first
474
+ else
475
+ if oauth_reuse_access_token
476
+ unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
477
+ valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
478
+ .where(unique_conds).first
479
+ return valid_token if valid_token
480
+ end
481
+ __insert_and_return__(ds, oauth_tokens_id_column, params)
482
+ end
483
+ end
484
+
485
+ def oauth_token_by_token_ds(token)
486
+ ds = db[oauth_tokens_table]
487
+
488
+ ds = if oauth_tokens_token_hash_column
489
+ ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_hash_column] => generate_token_hash(token))
490
+ else
491
+ ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_column] => token)
492
+ end
493
+
494
+ ds.where(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
495
+ .where(Sequel[oauth_tokens_table][oauth_tokens_revoked_at_column] => nil)
496
+ end
497
+
498
+ def oauth_token_by_token(token)
499
+ oauth_token_by_token_ds(token).first
500
+ end
501
+
502
+ def oauth_token_by_refresh_token(token, revoked: false)
503
+ ds = db[oauth_tokens_table]
504
+ #
505
+ # filter expired refresh tokens out.
506
+ # an expired refresh token is a token whose access token expired for a period longer than the
507
+ # refresh token expiration period.
508
+ #
509
+ ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
510
+
511
+ ds = if oauth_tokens_refresh_token_hash_column
512
+ ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
513
+ else
514
+ ds.where(oauth_tokens_refresh_token_column => token)
515
+ end
516
+
517
+ ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
518
+
519
+ ds.first
520
+ end
521
+
522
+ def json_access_token_payload(oauth_token)
523
+ payload = {
524
+ "access_token" => oauth_token[oauth_tokens_token_column],
525
+ "token_type" => oauth_token_type,
526
+ "expires_in" => oauth_token_expires_in
527
+ }
528
+ payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[oauth_tokens_refresh_token_column]
529
+ payload
530
+ end
531
+
532
+ # Access Tokens
533
+
534
+ def validate_oauth_token_params
535
+ unless (grant_type = param_or_nil("grant_type"))
536
+ redirect_response_error("invalid_request")
537
+ end
538
+
539
+ redirect_response_error("invalid_request") if grant_type == "refresh_token" && !param_or_nil("refresh_token")
540
+ end
541
+
542
+ def create_oauth_token(grant_type)
543
+ if supported_grant_type?(grant_type, "refresh_token")
544
+ # fetch potentially revoked oauth token
545
+ oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
546
+
547
+ if !oauth_token
548
+ redirect_response_error("invalid_grant")
549
+ elsif oauth_token[oauth_tokens_revoked_at_column]
550
+ if oauth_refresh_token_protection_policy == "rotation"
551
+ # https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
552
+ #
553
+ # If a refresh token is compromised and subsequently used by both the attacker and the legitimate
554
+ # client, one of them will present an invalidated refresh token, which will inform the authorization
555
+ # server of the breach. The authorization server cannot determine which party submitted the invalid
556
+ # refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
557
+ # forcing the legitimate client to obtain a fresh authorization grant.
558
+
559
+ db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
560
+ .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
561
+ end
562
+ redirect_response_error("invalid_grant")
563
+ end
564
+
565
+ update_params = {
566
+ oauth_tokens_oauth_application_id_column => oauth_token[oauth_tokens_oauth_application_id_column],
567
+ oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
568
+ }
569
+ create_oauth_token_from_token(oauth_token, update_params)
570
+ else
571
+ redirect_response_error("invalid_request")
572
+ end
573
+ end
574
+
575
+ def create_oauth_token_from_token(oauth_token, update_params)
576
+ redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
577
+
578
+ rescue_from_uniqueness_error do
579
+ oauth_tokens_ds = db[oauth_tokens_table]
580
+ token = oauth_unique_id_generator
581
+
582
+ if oauth_tokens_token_hash_column
583
+ update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
584
+ else
585
+ update_params[oauth_tokens_token_column] = token
586
+ end
587
+
588
+ oauth_token = if oauth_refresh_token_protection_policy == "rotation"
589
+ insert_params = {
590
+ **update_params,
591
+ oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
592
+ oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
593
+ }
594
+
595
+ refresh_token = oauth_unique_id_generator
596
+
597
+ if oauth_tokens_refresh_token_hash_column
598
+ insert_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
599
+ else
600
+ insert_params[oauth_tokens_refresh_token_column] = refresh_token
601
+ end
602
+
603
+ # revoke the refresh token
604
+ oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
605
+ .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
606
+
607
+ insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column]
608
+ __insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params)
609
+ else
610
+ # includes none
611
+ ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
612
+ __update_and_return__(ds, update_params)
613
+ end
614
+
615
+ oauth_token[oauth_tokens_token_column] = token
616
+ oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
617
+ oauth_token
618
+ end
619
+ end
620
+
621
+ def supported_grant_type?(grant_type, expected_grant_type = grant_type)
622
+ return false unless grant_type == expected_grant_type
623
+
624
+ return true unless (grant_types_supported = oauth_application[oauth_applications_grant_types_column])
625
+
626
+ grant_types_supported = grant_types_supported.split(/ +/)
627
+
628
+ grant_types_supported.include?(grant_type)
629
+ end
630
+
631
+ def oauth_server_metadata_body(path = nil)
632
+ issuer = base_url
633
+ issuer += "/#{path}" if path
634
+
635
+ {
636
+ issuer: issuer,
637
+ token_endpoint: token_url,
638
+ scopes_supported: oauth_application_scopes,
639
+ response_types_supported: [],
640
+ response_modes_supported: [],
641
+ grant_types_supported: %w[refresh_token],
642
+ token_endpoint_auth_methods_supported: oauth_auth_methods_supported,
643
+ service_documentation: oauth_metadata_service_documentation,
644
+ ui_locales_supported: oauth_metadata_ui_locales_supported,
645
+ op_policy_uri: oauth_metadata_op_policy_uri,
646
+ op_tos_uri: oauth_metadata_op_tos_uri
647
+ }
648
+ end
649
+
650
+ def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
651
+ if accepts_json?
652
+ status_code = if respond_to?(:"#{error_code}_response_status")
653
+ send(:"#{error_code}_response_status")
654
+ else
655
+ invalid_oauth_response_status
656
+ end
657
+
658
+ throw_json_response_error(status_code, error_code)
659
+ else
660
+ redirect_url = URI.parse(redirect_url)
661
+ query_params = []
662
+
663
+ query_params << if respond_to?(:"#{error_code}_error_code")
664
+ "error=#{send(:"#{error_code}_error_code")}"
665
+ else
666
+ "error=#{error_code}"
667
+ end
668
+
669
+ if respond_to?(:"#{error_code}_message")
670
+ message = send(:"#{error_code}_message")
671
+ query_params << ["error_description=#{CGI.escape(message)}"]
672
+ end
673
+
674
+ query_params << redirect_url.query if redirect_url.query
675
+ redirect_url.query = query_params.join("&")
676
+ redirect(redirect_url.to_s)
677
+ end
678
+ end
679
+
680
+ def json_response_success(body, cache = false)
681
+ response.status = 200
682
+ response["Content-Type"] ||= json_response_content_type
683
+ if cache
684
+ # defaulting to 1-day for everyone, for now at least
685
+ max_age = 60 * 60 * 24
686
+ response["Cache-Control"] = "private, max-age=#{max_age}"
687
+ else
688
+ response["Cache-Control"] = "no-store"
689
+ response["Pragma"] = "no-cache"
690
+ end
691
+ json_payload = _json_response_body(body)
692
+ return_response(json_payload)
693
+ end
694
+
695
+ def throw_json_response_error(status, error_code, message = nil)
696
+ set_response_error_status(status)
697
+ code = if respond_to?(:"#{error_code}_error_code")
698
+ send(:"#{error_code}_error_code")
699
+ else
700
+ error_code
701
+ end
702
+ payload = { "error" => code }
703
+ payload["error_description"] = message || (send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message"))
704
+ json_payload = _json_response_body(payload)
705
+ response["Content-Type"] ||= json_response_content_type
706
+ response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
707
+ return_response(json_payload)
708
+ end
709
+
710
+ unless method_defined?(:_json_response_body)
711
+ def _json_response_body(hash)
712
+ if request.respond_to?(:convert_to_json)
713
+ request.send(:convert_to_json, hash)
714
+ else
715
+ JSON.dump(hash)
716
+ end
717
+ end
718
+ end
719
+
720
+ if Gem::Version.new(Rodauth.version) < Gem::Version.new("2.23")
721
+ def return_response(body = nil)
722
+ response.write(body) if body
723
+ request.halt
724
+ end
725
+ end
726
+
727
+ def authorization_required
728
+ if accepts_json?
729
+ throw_json_response_error(authorization_required_error_status, "invalid_client")
730
+ else
731
+ set_redirect_error_flash(require_authorization_error_flash)
732
+ redirect(authorize_path)
733
+ end
734
+ end
735
+
736
+ def check_valid_scopes?
737
+ return false unless scopes
738
+
739
+ (scopes - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
740
+ end
741
+
742
+ def check_valid_uri?(uri)
743
+ URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
744
+ end
745
+
746
+ # Resource server mode
747
+
748
+ SERVER_METADATA = OAuth::TtlStore.new
749
+
750
+ def authorization_server_metadata
751
+ auth_url = URI(authorization_server_url)
752
+
753
+ server_metadata = SERVER_METADATA[auth_url]
754
+
755
+ return server_metadata if server_metadata
756
+
757
+ SERVER_METADATA.set(auth_url) do
758
+ http = Net::HTTP.new(auth_url.host, auth_url.port)
759
+ http.use_ssl = auth_url.scheme == "https"
760
+
761
+ request = Net::HTTP::Get.new("/.well-known/oauth-authorization-server")
762
+ request["accept"] = json_response_content_type
763
+ response = http.request(request)
764
+ authorization_required unless response.code.to_i == 200
765
+
766
+ # time-to-live
767
+ ttl = if response.key?("cache-control")
768
+ cache_control = response["cache-control"]
769
+ cache_control[/max-age=(\d+)/, 1].to_i
770
+ elsif response.key?("expires")
771
+ Time.parse(response["expires"]).to_i - Time.now.to_i
772
+ end
773
+
774
+ [JSON.parse(response.body, symbolize_names: true), ttl]
775
+ end
776
+ end
777
+ end
778
+ end