rodauth-oauth 0.7.3 → 0.9.0

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