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