rodauth-oauth 0.7.4 → 0.9.1

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