rodauth-oauth 0.7.3 → 0.9.0

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