rodauth-oauth 0.7.4 → 0.8.0

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