rodauth-oauth 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -407
  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/install_generator.rb +1 -1
  28. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +29 -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 +50 -0
  32. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +23 -0
  33. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +38 -0
  34. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +29 -0
  35. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +34 -0
  36. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +9 -1
  37. data/lib/generators/rodauth/oauth/views_generator.rb +9 -4
  38. data/lib/rodauth/features/oauth.rb +3 -1418
  39. data/lib/rodauth/features/oauth_application_management.rb +209 -0
  40. data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
  41. data/lib/rodauth/features/oauth_authorization_code_grant.rb +249 -0
  42. data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
  43. data/lib/rodauth/features/oauth_base.rb +735 -0
  44. data/lib/rodauth/features/oauth_device_grant.rb +221 -0
  45. data/lib/rodauth/features/oauth_http_mac.rb +3 -21
  46. data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
  47. data/lib/rodauth/features/oauth_jwt.rb +37 -60
  48. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
  49. data/lib/rodauth/features/oauth_pkce.rb +98 -0
  50. data/lib/rodauth/features/oauth_resource_server.rb +21 -0
  51. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
  52. data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
  53. data/lib/rodauth/features/oauth_token_management.rb +77 -0
  54. data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
  55. data/lib/rodauth/features/oidc.rb +4 -3
  56. data/lib/rodauth/oauth/database_extensions.rb +15 -2
  57. data/lib/rodauth/oauth/refinements.rb +48 -0
  58. data/lib/rodauth/oauth/version.rb +1 -1
  59. data/locales/en.yml +28 -12
  60. data/templates/authorize.str +7 -7
  61. data/templates/client_secret_field.str +2 -2
  62. data/templates/description_field.str +1 -1
  63. data/templates/device_search.str +11 -0
  64. data/templates/device_verification.str +24 -0
  65. data/templates/homepage_url_field.str +2 -2
  66. data/templates/jws_jwk_field.str +4 -0
  67. data/templates/jwt_public_key_field.str +4 -0
  68. data/templates/name_field.str +1 -1
  69. data/templates/new_oauth_application.str +9 -0
  70. data/templates/oauth_application.str +7 -3
  71. data/templates/oauth_application_oauth_tokens.str +51 -0
  72. data/templates/oauth_applications.str +2 -2
  73. data/templates/oauth_tokens.str +9 -11
  74. data/templates/redirect_uri_field.str +2 -2
  75. metadata +97 -7
  76. data/lib/rodauth/features/oauth_saml.rb +0 -104
@@ -1,1424 +1,9 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- require "time"
4
- require "base64"
5
- require "securerandom"
6
- require "net/http"
7
-
8
- require "rodauth/oauth/ttl_store"
9
- require "rodauth/oauth/database_extensions"
10
-
11
3
  module Rodauth
12
4
  Feature.define(:oauth, :Oauth) do
13
- # RUBY EXTENSIONS
14
- unless Regexp.method_defined?(:match?)
15
- # If you wonder why this is there: the oauth feature uses a refinement to enhance the
16
- # Regexp class locally with #match? , but this is never tested, because ActiveSupport
17
- # monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
18
- # :nocov:
19
- module RegexpExtensions
20
- refine(Regexp) do
21
- def match?(*args)
22
- !match(*args).nil?
23
- end
24
- end
25
- end
26
- using(RegexpExtensions)
27
- # :nocov:
28
- end
29
-
30
- unless String.method_defined?(:delete_suffix!)
31
- module SuffixExtensions
32
- refine(String) do
33
- def delete_suffix!(suffix)
34
- suffix = suffix.to_s
35
- chomp! if frozen?
36
- len = suffix.length
37
- return unless len.positive? && index(suffix, -len)
38
-
39
- self[-len..-1] = ""
40
- self
41
- end
42
- end
43
- end
44
- using(SuffixExtensions)
45
- end
46
-
47
- SCOPES = %w[profile.read].freeze
48
-
49
- SERVER_METADATA = OAuth::TtlStore.new
50
-
51
- before "authorize"
52
- after "authorize"
53
-
54
- before "token"
55
-
56
- before "revoke"
57
- after "revoke"
58
-
59
- before "introspect"
60
-
61
- before "create_oauth_application"
62
- after "create_oauth_application"
63
-
64
- error_flash "Please authorize to continue", "require_authorization"
65
- error_flash "There was an error registering your oauth application", "create_oauth_application"
66
- notice_flash "Your oauth application has been registered", "create_oauth_application"
67
-
68
- notice_flash "The oauth token has been revoked", "revoke_oauth_token"
69
- error_flash "You are not authorized to revoke this token", "revoke_unauthorized_account"
70
-
71
- view "authorize", "Authorize", "authorize"
72
- view "oauth_applications", "Oauth Applications", "oauth_applications"
73
- view "oauth_application", "Oauth Application", "oauth_application"
74
- view "new_oauth_application", "New Oauth Application", "new_oauth_application"
75
- view "oauth_tokens", "Oauth Tokens", "oauth_tokens"
76
-
77
- auth_value_method :json_response_content_type, "application/json"
78
-
79
- auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
80
- auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
81
- auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
82
- auth_value_method :use_oauth_implicit_grant_type?, false
83
- auth_value_method :use_oauth_pkce?, true
84
- auth_value_method :use_oauth_access_type?, true
85
-
86
- auth_value_method :oauth_require_pkce, false
87
- auth_value_method :oauth_pkce_challenge_method, "S256"
88
- auth_value_method :oauth_response_mode, "query"
89
-
90
- auth_value_method :oauth_valid_uri_schemes, %w[https]
91
-
92
- auth_value_method :oauth_scope_separator, " "
93
-
94
- # Application
95
- APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri client_secret].freeze
96
- auth_value_method :oauth_application_required_params, APPLICATION_REQUIRED_PARAMS
97
-
98
- (APPLICATION_REQUIRED_PARAMS + %w[client_id]).each do |param|
99
- auth_value_method :"oauth_application_#{param}_param", param
100
- translatable_method :"#{param}_label", param.gsub("_", " ").capitalize
101
- end
102
- button "Register", "oauth_application"
103
- button "Authorize", "oauth_authorize"
104
- button "Revoke", "oauth_token_revoke"
105
- button "Back to Client Application", "oauth_authorize_post"
106
-
107
- # OAuth Token
108
- auth_value_method :oauth_tokens_path, "oauth-tokens"
109
- auth_value_method :oauth_tokens_table, :oauth_tokens
110
- auth_value_method :oauth_tokens_id_column, :id
111
-
112
- %i[
113
- oauth_application_id oauth_token_id oauth_grant_id account_id
114
- token refresh_token scopes
115
- expires_in revoked_at
116
- ].each do |column|
117
- auth_value_method :"oauth_tokens_#{column}_column", column
118
- end
119
-
120
- # Oauth Token Hash
121
- auth_value_method :oauth_tokens_token_hash_column, nil
122
- auth_value_method :oauth_tokens_refresh_token_hash_column, nil
123
-
124
- # Access Token reuse
125
- auth_value_method :oauth_reuse_access_token, false
126
- # OAuth Grants
127
- auth_value_method :oauth_grants_table, :oauth_grants
128
- auth_value_method :oauth_grants_id_column, :id
129
- %i[
130
- account_id oauth_application_id
131
- redirect_uri code scopes access_type
132
- expires_in revoked_at
133
- code_challenge code_challenge_method
134
- ].each do |column|
135
- auth_value_method :"oauth_grants_#{column}_column", column
136
- end
137
-
138
- auth_value_method :authorization_required_error_status, 401
139
- auth_value_method :invalid_oauth_response_status, 400
140
- auth_value_method :already_in_use_response_status, 409
141
-
142
- # OAuth Applications
143
- auth_value_method :oauth_applications_route, "oauth-applications"
144
- def oauth_applications_path(opts = {})
145
- route_path(oauth_applications_route, opts)
146
- end
147
-
148
- def oauth_applications_url(opts = {})
149
- route_url(oauth_applications_route, opts)
150
- end
151
-
152
- auth_value_method :oauth_applications_table, :oauth_applications
153
-
154
- auth_value_method :oauth_applications_id_column, :id
155
- auth_value_method :oauth_applications_id_pattern, Integer
156
-
157
- %i[
158
- account_id
159
- name description scopes
160
- client_id client_secret
161
- homepage_url redirect_uri
162
- ].each do |column|
163
- auth_value_method :"oauth_applications_#{column}_column", column
164
- end
165
-
166
- # Feature options
167
- auth_value_method :oauth_application_default_scope, SCOPES.first
168
- auth_value_method :oauth_application_scopes, SCOPES
169
- auth_value_method :oauth_token_type, "bearer"
170
- auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
171
-
172
- translatable_method :invalid_client_message, "Invalid client"
173
- translatable_method :invalid_grant_type_message, "Invalid grant type"
174
- translatable_method :invalid_grant_message, "Invalid grant"
175
- translatable_method :invalid_scope_message, "Invalid scope"
176
-
177
- translatable_method :invalid_url_message, "Invalid URL"
178
- translatable_method :unsupported_token_type_message, "Invalid token type hint"
179
-
180
- translatable_method :unique_error_message, "is already in use"
181
- translatable_method :null_error_message, "is not filled"
182
- translatable_method :already_in_use_message, "error generating unique token"
183
- auth_value_method :already_in_use_error_code, "invalid_request"
184
-
185
- # PKCE
186
- auth_value_method :code_challenge_required_error_code, "invalid_request"
187
- translatable_method :code_challenge_required_message, "code challenge required"
188
- auth_value_method :unsupported_transform_algorithm_error_code, "invalid_request"
189
- translatable_method :unsupported_transform_algorithm_message, "transform algorithm not supported"
190
-
191
- # METADATA
192
- auth_value_method :oauth_metadata_service_documentation, nil
193
- auth_value_method :oauth_metadata_ui_locales_supported, nil
194
- auth_value_method :oauth_metadata_op_policy_uri, nil
195
- auth_value_method :oauth_metadata_op_tos_uri, nil
196
-
197
- # Resource Server params
198
- # Only required to use if the plugin is to be used in a resource server
199
- auth_value_method :is_authorization_server?, true
200
-
201
- auth_value_method :oauth_unique_id_generation_retries, 3
202
-
203
- auth_value_methods(
204
- :oauth_application_path,
205
- :fetch_access_token,
206
- :oauth_unique_id_generator,
207
- :secret_matches?,
208
- :secret_hash,
209
- :generate_token_hash,
210
- :authorization_server_url,
211
- :before_introspection_request,
212
- :require_authorizable_account,
213
- :oauth_tokens_unique_columns
214
- )
215
-
216
- auth_value_methods(:only_json?)
217
-
218
- auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
219
-
220
- # /token
221
- route(:token) do |r|
222
- next unless is_authorization_server?
223
-
224
- before_token_route
225
- require_oauth_application
226
-
227
- r.post do
228
- catch_error do
229
- validate_oauth_token_params
230
-
231
- oauth_token = nil
232
- transaction do
233
- before_token
234
- oauth_token = create_oauth_token
235
- end
236
-
237
- json_response_success(json_access_token_payload(oauth_token))
238
- end
239
-
240
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
241
- end
242
- end
243
-
244
- # /introspect
245
- route(:introspect) do |r|
246
- next unless is_authorization_server?
247
-
248
- before_introspect_route
249
-
250
- r.post do
251
- catch_error do
252
- validate_oauth_introspect_params
253
-
254
- before_introspect
255
- oauth_token = case param("token_type_hint")
256
- when "access_token"
257
- oauth_token_by_token(param("token"))
258
- when "refresh_token"
259
- oauth_token_by_refresh_token(param("token"))
260
- else
261
- oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
262
- end
263
-
264
- if oauth_application
265
- redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
266
- elsif oauth_token
267
- @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
268
- oauth_token[oauth_tokens_oauth_application_id_column]).first
269
- end
270
-
271
- json_response_success(json_token_introspect_payload(oauth_token))
272
- end
273
-
274
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
275
- end
276
- end
277
-
278
- # /revoke
279
- route(:revoke) do |r|
280
- next unless is_authorization_server?
281
-
282
- before_revoke_route
283
-
284
- if logged_in?
285
- require_account
286
- require_oauth_application_from_account
287
- else
288
- require_oauth_application
289
- end
290
-
291
- r.post do
292
- catch_error do
293
- validate_oauth_revoke_params
294
-
295
- oauth_token = nil
296
- transaction do
297
- before_revoke
298
- oauth_token = revoke_oauth_token
299
- after_revoke
300
- end
301
-
302
- if accepts_json?
303
- json_response_success \
304
- "token" => oauth_token[oauth_tokens_token_column],
305
- "refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
306
- "revoked_at" => convert_timestamp(oauth_token[oauth_tokens_revoked_at_column])
307
- else
308
- set_notice_flash revoke_oauth_token_notice_flash
309
- redirect request.referer || "/"
310
- end
311
- end
312
-
313
- redirect_response_error("invalid_request", request.referer || "/")
314
- end
315
- end
316
-
317
- # /authorize
318
- route(:authorize) do |r|
319
- next unless is_authorization_server?
320
-
321
- before_authorize_route
322
- require_authorizable_account
323
-
324
- validate_oauth_grant_params
325
- try_approval_prompt if use_oauth_access_type? && request.get?
326
-
327
- r.get do
328
- authorize_view
329
- end
330
-
331
- r.post do
332
- redirect_url = URI.parse(redirect_uri)
333
-
334
- params, mode = transaction do
335
- before_authorize
336
- do_authorize
337
- end
338
-
339
- case mode
340
- when "query"
341
- params = params.map { |k, v| "#{k}=#{v}" }
342
- params << redirect_url.query if redirect_url.query
343
- redirect_url.query = params.join("&")
344
- redirect(redirect_url.to_s)
345
- when "fragment"
346
- params = params.map { |k, v| "#{k}=#{v}" }
347
- params << redirect_url.query if redirect_url.query
348
- redirect_url.fragment = params.join("&")
349
- redirect(redirect_url.to_s)
350
- when "form_post"
351
- scope.view layout: false, inline: <<-FORM
352
- <html>
353
- <head><title>Authorized</title></head>
354
- <body onload="javascript:document.forms[0].submit()">
355
- <form method="post" action="#{redirect_uri}">
356
- #{
357
- params.map do |name, value|
358
- "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
359
- end.join
360
- }
361
- <input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
362
- </form>
363
- </body>
364
- </html>
365
- FORM
366
- when "none"
367
- redirect(redirect_url.to_s)
368
- end
369
- end
370
- end
371
-
372
- def oauth_server_metadata(issuer = nil)
373
- request.on(".well-known") do
374
- request.on("oauth-authorization-server") do
375
- request.get do
376
- json_response_success(oauth_server_metadata_body(issuer), true)
377
- end
378
- end
379
- end
380
- end
381
-
382
- def oauth_application_path(id)
383
- "#{oauth_applications_path}/#{id}"
384
- end
385
-
386
- # /oauth-applications routes
387
- def oauth_applications
388
- request.on(oauth_applications_route) do
389
- require_account
390
-
391
- request.get "new" do
392
- new_oauth_application_view
393
- end
394
-
395
- request.on(oauth_applications_id_pattern) do |id|
396
- oauth_application = db[oauth_applications_table]
397
- .where(oauth_applications_id_column => id)
398
- .where(oauth_applications_account_id_column => account_id)
399
- .first
400
- next unless oauth_application
401
-
402
- scope.instance_variable_set(:@oauth_application, oauth_application)
403
-
404
- request.is do
405
- request.get do
406
- oauth_application_view
407
- end
408
- end
409
-
410
- request.on(oauth_tokens_path) do
411
- oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
412
- scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
413
- request.get do
414
- oauth_tokens_view
415
- end
416
- end
417
- end
418
-
419
- request.get do
420
- scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table]
421
- .where(oauth_applications_account_id_column => account_id))
422
- oauth_applications_view
423
- end
424
-
425
- request.post do
426
- catch_error do
427
- validate_oauth_application_params
428
-
429
- transaction do
430
- before_create_oauth_application
431
- id = create_oauth_application
432
- after_create_oauth_application
433
- set_notice_flash create_oauth_application_notice_flash
434
- redirect "#{request.path}/#{id}"
435
- end
436
- end
437
- set_error_flash create_oauth_application_error_flash
438
- new_oauth_application_view
439
- end
440
- end
441
- end
442
-
443
- def check_csrf?
444
- case request.path
445
- when token_path, introspect_path
446
- false
447
- when revoke_path
448
- !json_request?
449
- when authorize_path, oauth_applications_path
450
- only_json? ? false : super
451
- else
452
- super
453
- end
454
- end
455
-
456
- # Overrides session_value, so that a valid authorization token also authenticates a request
457
- def session_value
458
- super || begin
459
- return unless authorization_token
460
-
461
- authorization_token[oauth_tokens_account_id_column]
462
- end
463
- end
464
-
465
- def accepts_json?
466
- return true if only_json?
467
-
468
- (accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
469
- end
470
-
471
- unless method_defined?(:json_request?)
472
- # copied from the jwt feature
473
- def json_request?
474
- return @json_request if defined?(@json_request)
475
-
476
- @json_request = request.content_type =~ json_request_regexp
477
- end
478
- end
479
-
480
- def scopes
481
- scope = request.params["scope"]
482
- case scope
483
- when Array
484
- scope
485
- when String
486
- scope.split(" ")
487
- when nil
488
- Array(oauth_application_default_scope)
489
- end
490
- end
491
-
492
- def redirect_uri
493
- param_or_nil("redirect_uri") || begin
494
- return unless oauth_application
495
-
496
- redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
497
- redirect_uris.size == 1 ? redirect_uris.first : nil
498
- end
499
- end
500
-
501
- def oauth_application
502
- return @oauth_application if defined?(@oauth_application)
503
-
504
- @oauth_application = begin
505
- client_id = param_or_nil("client_id")
506
-
507
- return unless client_id
508
-
509
- db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first
510
- end
511
- end
512
-
513
- def fetch_access_token
514
- value = request.env["HTTP_AUTHORIZATION"]
515
-
516
- return unless value && !value.empty?
517
-
518
- scheme, token = value.split(" ", 2)
519
-
520
- return unless scheme.downcase == oauth_token_type
521
-
522
- return if token.nil? || token.empty?
523
-
524
- token
525
- end
526
-
527
- def authorization_token
528
- return @authorization_token if defined?(@authorization_token)
529
-
530
- # check if there is a token
531
- bearer_token = fetch_access_token
532
-
533
- return unless bearer_token
534
-
535
- @authorization_token = if is_authorization_server?
536
- # check if token has not expired
537
- # check if token has been revoked
538
- oauth_token_by_token(bearer_token)
539
- else
540
- # where in resource server, NOT the authorization server.
541
- payload = introspection_request("access_token", bearer_token)
542
-
543
- return unless payload["active"]
544
-
545
- payload
546
- end
547
- end
548
-
549
- def require_oauth_authorization(*scopes)
550
- authorization_required unless authorization_token
551
-
552
- scopes << oauth_application_default_scope if scopes.empty?
553
-
554
- token_scopes = if is_authorization_server?
555
- authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
556
- else
557
- aux_scopes = authorization_token["scope"]
558
- if aux_scopes
559
- aux_scopes.split(oauth_scope_separator)
560
- else
561
- []
562
- end
563
- end
564
-
565
- authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
566
- end
567
-
568
- def post_configure
569
- super
570
-
571
- # all of the extensions below involve DB changes. Resource server mode doesn't use
572
- # database functions for OAuth though.
573
- return unless is_authorization_server?
574
-
575
- self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
576
-
577
- # Check whether we can reutilize db entries for the same account / application pair
578
- one_oauth_token_per_account = db.indexes(oauth_tokens_table).values.any? do |definition|
579
- definition[:unique] &&
580
- definition[:columns] == oauth_tokens_unique_columns
581
- end
582
-
583
- self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
584
-
585
- i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n)
586
- end
587
-
588
- def use_date_arithmetic?
589
- true
590
- end
591
-
592
- private
593
-
594
- def rescue_from_uniqueness_error(&block)
595
- retries = oauth_unique_id_generation_retries
596
- begin
597
- transaction(savepoint: :only, &block)
598
- rescue Sequel::UniqueConstraintViolation
599
- redirect_response_error("already_in_use") if retries.zero?
600
- retries -= 1
601
- retry
602
- end
603
- end
604
-
605
- # OAuth Token Unique/Reuse
606
- def oauth_tokens_unique_columns
607
- [
608
- oauth_tokens_oauth_application_id_column,
609
- oauth_tokens_account_id_column,
610
- oauth_tokens_scopes_column
611
- ]
612
- end
613
-
614
- def authorization_server_url
615
- base_url
616
- end
617
-
618
- def authorization_server_metadata
619
- auth_url = URI(authorization_server_url)
620
-
621
- server_metadata = SERVER_METADATA[auth_url]
622
-
623
- return server_metadata if server_metadata
624
-
625
- SERVER_METADATA.set(auth_url) do
626
- http = Net::HTTP.new(auth_url.host, auth_url.port)
627
- http.use_ssl = auth_url.scheme == "https"
628
-
629
- request = Net::HTTP::Get.new("/.well-known/oauth-authorization-server")
630
- request["accept"] = json_response_content_type
631
- response = http.request(request)
632
- authorization_required unless response.code.to_i == 200
633
-
634
- # time-to-live
635
- ttl = if response.key?("cache-control")
636
- cache_control = response["cache-control"]
637
- cache_control[/max-age=(\d+)/, 1].to_i
638
- elsif response.key?("expires")
639
- Time.parse(response["expires"]).to_i - Time.now.to_i
640
- end
641
-
642
- [JSON.parse(response.body, symbolize_names: true), ttl]
643
- end
644
- end
645
-
646
- def introspection_request(token_type_hint, token)
647
- auth_url = URI(authorization_server_url)
648
- http = Net::HTTP.new(auth_url.host, auth_url.port)
649
- http.use_ssl = auth_url.scheme == "https"
650
-
651
- request = Net::HTTP::Post.new(introspect_path)
652
- request["content-type"] = "application/x-www-form-urlencoded"
653
- request["accept"] = json_response_content_type
654
- request.set_form_data({ "token_type_hint" => token_type_hint, "token" => token })
655
-
656
- before_introspection_request(request)
657
- response = http.request(request)
658
- authorization_required unless response.code.to_i == 200
659
-
660
- JSON.parse(response.body)
661
- end
662
-
663
- def before_introspection_request(request); end
664
-
665
- def template_path(page)
666
- path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
667
- return super unless File.exist?(path)
668
-
669
- path
670
- end
671
-
672
- # to be used internally. Same semantics as require account, must:
673
- # fetch an authorization basic header
674
- # parse client id and secret
675
- #
676
- def require_oauth_application
677
- # get client credenntials
678
- client_id = client_secret = nil
679
-
680
- # client_secret_basic
681
- if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
682
- client_id, client_secret = Base64.decode64(token).split(/:/, 2)
683
- else
684
- client_id = param_or_nil("client_id")
685
- client_secret = param_or_nil("client_secret")
686
- end
687
-
688
- authorization_required unless client_id
689
-
690
- @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
691
-
692
- # skip if using pkce
693
- return if @oauth_application && use_oauth_pkce? && param_or_nil("code_verifier")
694
-
695
- authorization_required unless @oauth_application && secret_matches?(@oauth_application, client_secret)
696
- end
697
-
698
- def require_oauth_application_from_account
699
- ds = db[oauth_applications_table]
700
- .join(oauth_tokens_table, Sequel[oauth_tokens_table][oauth_tokens_oauth_application_id_column] =>
701
- Sequel[oauth_applications_table][oauth_applications_id_column])
702
- .where(oauth_token_by_token_ds(param("token")).opts.fetch(:where, true))
703
- .where(Sequel[oauth_applications_table][oauth_applications_account_id_column] => account_id)
704
-
705
- @oauth_application = ds.qualify.first
706
- return if @oauth_application
707
-
708
- set_redirect_error_flash revoke_unauthorized_account_error_flash
709
- redirect request.referer || "/"
710
- end
711
-
712
- def secret_matches?(oauth_application, secret)
713
- BCrypt::Password.new(oauth_application[oauth_applications_client_secret_column]) == secret
714
- end
715
-
716
- def secret_hash(secret)
717
- password_hash(secret)
718
- end
719
-
720
- def oauth_unique_id_generator
721
- SecureRandom.urlsafe_base64(32)
722
- end
723
-
724
- def generate_token_hash(token)
725
- Base64.urlsafe_encode64(Digest::SHA256.digest(token))
726
- end
727
-
728
- def token_from_application?(oauth_token, oauth_application)
729
- oauth_token[oauth_tokens_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
730
- end
731
-
732
- unless method_defined?(:password_hash)
733
- # From login_requirements_base feature
734
-
735
- def password_hash(password)
736
- BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
737
- end
738
- end
739
-
740
- def generate_oauth_token(params = {}, should_generate_refresh_token = true)
741
- create_params = {
742
- oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
743
- }.merge(params)
744
-
745
- rescue_from_uniqueness_error do
746
- token = oauth_unique_id_generator
747
-
748
- if oauth_tokens_token_hash_column
749
- create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
750
- else
751
- create_params[oauth_tokens_token_column] = token
752
- end
753
-
754
- refresh_token = nil
755
- if should_generate_refresh_token
756
- refresh_token = oauth_unique_id_generator
757
-
758
- if oauth_tokens_refresh_token_hash_column
759
- create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
760
- else
761
- create_params[oauth_tokens_refresh_token_column] = refresh_token
762
- end
763
- end
764
- oauth_token = _generate_oauth_token(create_params)
765
- oauth_token[oauth_tokens_token_column] = token
766
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
767
- oauth_token
768
- end
769
- end
770
-
771
- def _generate_oauth_token(params = {})
772
- ds = db[oauth_tokens_table]
773
-
774
- if __one_oauth_token_per_account
775
-
776
- token = __insert_or_update_and_return__(
777
- ds,
778
- oauth_tokens_id_column,
779
- oauth_tokens_unique_columns,
780
- params,
781
- Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
782
- ([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
783
- )
784
-
785
- # if the previous operation didn't return a row, it means that the conditions
786
- # invalidated the update, and the existing token is still valid.
787
- token || ds.where(
788
- oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
789
- oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
790
- ).first
791
- else
792
- if oauth_reuse_access_token
793
- unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
794
- valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
795
- .where(unique_conds).first
796
- return valid_token if valid_token
797
- end
798
- __insert_and_return__(ds, oauth_tokens_id_column, params)
799
- end
800
- end
801
-
802
- def oauth_token_by_token_ds(token)
803
- ds = db[oauth_tokens_table]
804
-
805
- ds = if oauth_tokens_token_hash_column
806
- ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_hash_column] => generate_token_hash(token))
807
- else
808
- ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_column] => token)
809
- end
810
-
811
- ds.where(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
812
- .where(Sequel[oauth_tokens_table][oauth_tokens_revoked_at_column] => nil)
813
- end
814
-
815
- def oauth_token_by_token(token)
816
- oauth_token_by_token_ds(token).first
817
- end
818
-
819
- def oauth_token_by_refresh_token(token, revoked: false)
820
- ds = db[oauth_tokens_table]
821
- #
822
- # filter expired refresh tokens out.
823
- # an expired refresh token is a token whose access token expired for a period longer than the
824
- # refresh token expiration period.
825
- #
826
- ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
827
-
828
- ds = if oauth_tokens_refresh_token_hash_column
829
- ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
830
- else
831
- ds.where(oauth_tokens_refresh_token_column => token)
832
- end
833
-
834
- ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
835
-
836
- ds.first
837
- end
838
-
839
- def json_access_token_payload(oauth_token)
840
- payload = {
841
- "access_token" => oauth_token[oauth_tokens_token_column],
842
- "token_type" => oauth_token_type,
843
- "expires_in" => oauth_token_expires_in
844
- }
845
- payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[oauth_tokens_refresh_token_column]
846
- payload
847
- end
848
-
849
- # Oauth Application
850
-
851
- def oauth_application_params
852
- @oauth_application_params ||= oauth_application_required_params.each_with_object({}) do |param, params|
853
- value = request.params[__send__(:"oauth_application_#{param}_param")]
854
- if value && !value.empty?
855
- params[param] = value
856
- else
857
- set_field_error(param, null_error_message)
858
- end
859
- end
860
- end
861
-
862
- def validate_oauth_application_params
863
- oauth_application_params.each do |key, value|
864
- if key == oauth_application_homepage_url_param
865
-
866
- set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
867
-
868
- elsif key == oauth_application_redirect_uri_param
869
-
870
- if value.respond_to?(:each)
871
- value.each do |uri|
872
- next if uri.empty?
873
-
874
- set_field_error(key, invalid_url_message) unless check_valid_uri?(uri)
875
- end
876
- else
877
- set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
878
- end
879
- elsif key == oauth_application_scopes_param
880
-
881
- value.each do |scope|
882
- set_field_error(key, invalid_scope_message) unless oauth_application_scopes.include?(scope)
883
- end
884
- end
885
- end
886
-
887
- throw :rodauth_error if @field_errors && !@field_errors.empty?
888
- end
889
-
890
- def create_oauth_application
891
- create_params = {
892
- oauth_applications_account_id_column => account_id,
893
- oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
894
- oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
895
- oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
896
- oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
897
- }
898
-
899
- redirect_uris = oauth_application_params[oauth_application_redirect_uri_param]
900
- redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each)
901
- create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty?
902
- # set client ID/secret pairs
903
-
904
- create_params.merge! \
905
- oauth_applications_client_secret_column => \
906
- secret_hash(oauth_application_params[oauth_application_client_secret_param])
907
-
908
- create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column]
909
- create_params[oauth_applications_scopes_column].join(oauth_scope_separator)
910
- else
911
- oauth_application_default_scope
912
- end
913
-
914
- rescue_from_uniqueness_error do
915
- create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
916
- db[oauth_applications_table].insert(create_params)
917
- end
918
- end
919
-
920
- # Authorize
921
- def require_authorizable_account
922
- require_account
923
- end
924
-
925
- def validate_oauth_grant_params
926
- redirect_response_error("invalid_request", request.referer || default_redirect) unless oauth_application && check_valid_redirect_uri?
927
-
928
- unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? &&
929
- check_valid_approval_prompt? && check_valid_response_type?
930
- redirect_response_error("invalid_request")
931
- end
932
- redirect_response_error("invalid_scope") unless check_valid_scopes?
933
-
934
- if (response_mode = param_or_nil("response_mode")) && response_mode != "form_post"
935
- redirect_response_error("invalid_request")
936
- end
937
- validate_pkce_challenge_params if use_oauth_pkce?
938
- end
939
-
940
- def try_approval_prompt
941
- approval_prompt = param_or_nil("approval_prompt")
942
-
943
- return unless approval_prompt && approval_prompt == "auto"
944
-
945
- return if db[oauth_grants_table].where(
946
- oauth_grants_account_id_column => account_id,
947
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
948
- oauth_grants_redirect_uri_column => redirect_uri,
949
- oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
950
- oauth_grants_access_type_column => "online"
951
- ).count.zero?
952
-
953
- # if there's a previous oauth grant for the params combo, it means that this user has approved before.
954
- request.env["REQUEST_METHOD"] = "POST"
955
- end
956
-
957
- def create_oauth_grant(create_params = {})
958
- create_params.merge!(
959
- oauth_grants_account_id_column => account_id,
960
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
961
- oauth_grants_redirect_uri_column => redirect_uri,
962
- oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
963
- oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
964
- )
965
-
966
- # Access Type flow
967
- if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
968
- create_params[oauth_grants_access_type_column] = access_type
969
- end
970
-
971
- # PKCE flow
972
- if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
973
- code_challenge_method = param_or_nil("code_challenge_method")
974
-
975
- create_params[oauth_grants_code_challenge_column] = code_challenge
976
- create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
977
- end
978
-
979
- ds = db[oauth_grants_table]
980
-
981
- rescue_from_uniqueness_error do
982
- create_params[oauth_grants_code_column] = oauth_unique_id_generator
983
- __insert_and_return__(ds, oauth_grants_id_column, create_params)
984
- end
985
- create_params[oauth_grants_code_column]
986
- end
987
-
988
- def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
989
- case param("response_type")
990
- when "token"
991
- redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
992
-
993
- response_mode ||= "fragment"
994
- response_params.replace(_do_authorize_token)
995
- when "code"
996
- response_mode ||= "query"
997
- response_params.replace(_do_authorize_code)
998
- when "none"
999
- response_mode ||= "none"
1000
- when "", nil
1001
- response_mode ||= oauth_response_mode
1002
- response_params.replace(_do_authorize_code)
1003
- end
1004
-
1005
- response_params["state"] = param("state") if param_or_nil("state")
1006
-
1007
- [response_params, response_mode]
1008
- end
1009
-
1010
- def _do_authorize_code
1011
- { "code" => create_oauth_grant }
1012
- end
1013
-
1014
- def _do_authorize_token
1015
- create_params = {
1016
- oauth_tokens_account_id_column => account_id,
1017
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
1018
- oauth_tokens_scopes_column => scopes
1019
- }
1020
- oauth_token = generate_oauth_token(create_params, false)
1021
-
1022
- json_access_token_payload(oauth_token)
1023
- end
1024
-
1025
- # Access Tokens
1026
-
1027
- def validate_oauth_token_params
1028
- unless (grant_type = param_or_nil("grant_type"))
1029
- redirect_response_error("invalid_request")
1030
- end
1031
-
1032
- case grant_type
1033
- when "authorization_code"
1034
- redirect_response_error("invalid_request") unless param_or_nil("code")
1035
-
1036
- when "refresh_token"
1037
- redirect_response_error("invalid_request") unless param_or_nil("refresh_token")
1038
- else
1039
- redirect_response_error("invalid_request")
1040
- end
1041
- end
1042
-
1043
- def create_oauth_token
1044
- case param("grant_type")
1045
- when "authorization_code"
1046
- # fetch oauth grant
1047
- oauth_grant = db[oauth_grants_table].where(
1048
- oauth_grants_code_column => param("code"),
1049
- oauth_grants_redirect_uri_column => param("redirect_uri"),
1050
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
1051
- oauth_grants_revoked_at_column => nil
1052
- ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
1053
- .for_update
1054
- .first
1055
-
1056
- redirect_response_error("invalid_grant") unless oauth_grant
1057
-
1058
- create_params = {
1059
- oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
1060
- oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
1061
- oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
1062
- oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
1063
- }
1064
- create_oauth_token_from_authorization_code(oauth_grant, create_params)
1065
- when "refresh_token"
1066
- # fetch potentially revoked oauth token
1067
- oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
1068
-
1069
- if !oauth_token
1070
- redirect_response_error("invalid_grant")
1071
- elsif oauth_token[oauth_tokens_revoked_at_column]
1072
- if oauth_refresh_token_protection_policy == "rotation"
1073
- # https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
1074
- #
1075
- # If a refresh token is compromised and subsequently used by both the attacker and the legitimate
1076
- # client, one of them will present an invalidated refresh token, which will inform the authorization
1077
- # server of the breach. The authorization server cannot determine which party submitted the invalid
1078
- # refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
1079
- # forcing the legitimate client to obtain a fresh authorization grant.
1080
-
1081
- db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
1082
- .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
1083
- end
1084
- redirect_response_error("invalid_grant")
1085
- end
1086
-
1087
- update_params = {
1088
- oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
1089
- oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
1090
- }
1091
- create_oauth_token_from_token(oauth_token, update_params)
1092
- end
1093
- end
1094
-
1095
- def create_oauth_token_from_authorization_code(oauth_grant, create_params)
1096
- # PKCE
1097
- if use_oauth_pkce?
1098
- if oauth_grant[oauth_grants_code_challenge_column]
1099
- code_verifier = param_or_nil("code_verifier")
1100
-
1101
- redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
1102
- elsif oauth_require_pkce
1103
- redirect_response_error("code_challenge_required")
1104
- end
1105
- end
1106
-
1107
- # revoke oauth grant
1108
- db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
1109
- .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
1110
-
1111
- should_generate_refresh_token = !use_oauth_access_type? ||
1112
- oauth_grant[oauth_grants_access_type_column] == "offline"
1113
-
1114
- generate_oauth_token(create_params, should_generate_refresh_token)
1115
- end
1116
-
1117
- def create_oauth_token_from_token(oauth_token, update_params)
1118
- redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
1119
-
1120
- rescue_from_uniqueness_error do
1121
- oauth_tokens_ds = db[oauth_tokens_table]
1122
- token = oauth_unique_id_generator
1123
-
1124
- if oauth_tokens_token_hash_column
1125
- update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
1126
- else
1127
- update_params[oauth_tokens_token_column] = token
1128
- end
1129
-
1130
- oauth_token = if oauth_refresh_token_protection_policy == "rotation"
1131
- insert_params = {
1132
- **update_params,
1133
- oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
1134
- oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
1135
- }
1136
-
1137
- refresh_token = oauth_unique_id_generator
1138
-
1139
- if oauth_tokens_refresh_token_hash_column
1140
- insert_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
1141
- else
1142
- insert_params[oauth_tokens_refresh_token_column] = refresh_token
1143
- end
1144
-
1145
- # revoke the refresh token
1146
- oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
1147
- .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
1148
-
1149
- insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column]
1150
- __insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params)
1151
- else
1152
- # includes none
1153
- ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
1154
- __update_and_return__(ds, update_params)
1155
- end
1156
-
1157
- oauth_token[oauth_tokens_token_column] = token
1158
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
1159
- oauth_token
1160
- end
1161
- end
1162
-
1163
- TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
1164
-
1165
- # Token introspect
1166
-
1167
- def validate_oauth_introspect_params
1168
- # check if valid token hint type
1169
- if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1170
- redirect_response_error("unsupported_token_type")
1171
- end
1172
-
1173
- redirect_response_error("invalid_request") unless param_or_nil("token")
1174
- end
1175
-
1176
- def json_token_introspect_payload(token)
1177
- return { active: false } unless token
1178
-
1179
- {
1180
- active: true,
1181
- scope: token[oauth_tokens_scopes_column],
1182
- client_id: oauth_application[oauth_applications_client_id_column],
1183
- # username
1184
- token_type: oauth_token_type,
1185
- exp: token[oauth_tokens_expires_in_column].to_i
1186
- }
1187
- end
1188
-
1189
- # Token revocation
1190
-
1191
- def validate_oauth_revoke_params
1192
- # check if valid token hint type
1193
- if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1194
- redirect_response_error("unsupported_token_type")
1195
- end
1196
-
1197
- redirect_response_error("invalid_request") unless param_or_nil("token")
1198
- end
1199
-
1200
- def revoke_oauth_token
1201
- token = param("token")
1202
-
1203
- oauth_token = if param("token_type_hint") == "refresh_token"
1204
- oauth_token_by_refresh_token(token)
1205
- else
1206
- oauth_token_by_token(token)
1207
- end
1208
-
1209
- redirect_response_error("invalid_request") unless oauth_token
1210
-
1211
- redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
1212
-
1213
- update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
1214
-
1215
- ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
1216
-
1217
- oauth_token = __update_and_return__(ds, update_params)
1218
-
1219
- oauth_token[oauth_tokens_token_column] = token
1220
- oauth_token
1221
-
1222
- # If the particular
1223
- # token is a refresh token and the authorization server supports the
1224
- # revocation of access tokens, then the authorization server SHOULD
1225
- # also invalidate all access tokens based on the same authorization
1226
- # grant
1227
- #
1228
- # we don't need to do anything here, as we revalidate existing tokens
1229
- end
1230
-
1231
- # Response helpers
1232
-
1233
- def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
1234
- if accepts_json?
1235
- status_code = if respond_to?(:"#{error_code}_response_status")
1236
- send(:"#{error_code}_response_status")
1237
- else
1238
- invalid_oauth_response_status
1239
- end
1240
-
1241
- throw_json_response_error(status_code, error_code)
1242
- else
1243
- redirect_url = URI.parse(redirect_url)
1244
- query_params = []
1245
-
1246
- query_params << if respond_to?(:"#{error_code}_error_code")
1247
- "error=#{send(:"#{error_code}_error_code")}"
1248
- else
1249
- "error=#{error_code}"
1250
- end
1251
-
1252
- if respond_to?(:"#{error_code}_message")
1253
- message = send(:"#{error_code}_message")
1254
- query_params << ["error_description=#{CGI.escape(message)}"]
1255
- end
1256
-
1257
- query_params << redirect_url.query if redirect_url.query
1258
- redirect_url.query = query_params.join("&")
1259
- redirect(redirect_url.to_s)
1260
- end
1261
- end
1262
-
1263
- def json_response_success(body, cache = false)
1264
- response.status = 200
1265
- response["Content-Type"] ||= json_response_content_type
1266
- if cache
1267
- # defaulting to 1-day for everyone, for now at least
1268
- max_age = 60 * 60 * 24
1269
- response["Cache-Control"] = "private, max-age=#{max_age}"
1270
- else
1271
- response["Cache-Control"] = "no-store"
1272
- response["Pragma"] = "no-cache"
1273
- end
1274
- json_payload = _json_response_body(body)
1275
- response.write(json_payload)
1276
- request.halt
1277
- end
1278
-
1279
- def throw_json_response_error(status, error_code)
1280
- set_response_error_status(status)
1281
- code = if respond_to?(:"#{error_code}_error_code")
1282
- send(:"#{error_code}_error_code")
1283
- else
1284
- error_code
1285
- end
1286
- payload = { "error" => code }
1287
- payload["error_description"] = send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")
1288
- json_payload = _json_response_body(payload)
1289
- response["Content-Type"] ||= json_response_content_type
1290
- response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
1291
- response.write(json_payload)
1292
- request.halt
1293
- end
1294
-
1295
- unless method_defined?(:_json_response_body)
1296
- def _json_response_body(hash)
1297
- if request.respond_to?(:convert_to_json)
1298
- request.send(:convert_to_json, hash)
1299
- else
1300
- JSON.dump(hash)
1301
- end
1302
- end
1303
- end
1304
-
1305
- def authorization_required
1306
- if accepts_json?
1307
- throw_json_response_error(authorization_required_error_status, "invalid_client")
1308
- else
1309
- set_redirect_error_flash(require_authorization_error_flash)
1310
- redirect(authorize_path)
1311
- end
1312
- end
1313
-
1314
- def check_valid_uri?(uri)
1315
- URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
1316
- end
1317
-
1318
- def check_valid_scopes?
1319
- return false unless scopes
1320
-
1321
- (scopes - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
1322
- end
1323
-
1324
- def check_valid_redirect_uri?
1325
- oauth_application[oauth_applications_redirect_uri_column].split(" ").include?(redirect_uri)
1326
- end
1327
-
1328
- ACCESS_TYPES = %w[offline online].freeze
1329
-
1330
- def check_valid_access_type?
1331
- return true unless use_oauth_access_type?
1332
-
1333
- access_type = param_or_nil("access_type")
1334
- !access_type || ACCESS_TYPES.include?(access_type)
1335
- end
1336
-
1337
- APPROVAL_PROMPTS = %w[force auto].freeze
1338
-
1339
- def check_valid_approval_prompt?
1340
- return true unless use_oauth_access_type?
1341
-
1342
- approval_prompt = param_or_nil("approval_prompt")
1343
- !approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
1344
- end
1345
-
1346
- def check_valid_response_type?
1347
- response_type = param_or_nil("response_type")
1348
-
1349
- return true if response_type.nil? || response_type == "code"
1350
-
1351
- return use_oauth_implicit_grant_type? if response_type == "token"
1352
-
1353
- false
1354
- end
1355
-
1356
- # PKCE
1357
-
1358
- def validate_pkce_challenge_params
1359
- if param_or_nil("code_challenge")
1360
-
1361
- challenge_method = param_or_nil("code_challenge_method")
1362
- redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
1363
- else
1364
- return unless oauth_require_pkce
1365
-
1366
- redirect_response_error("code_challenge_required")
1367
- end
1368
- end
1369
-
1370
- def check_valid_grant_challenge?(grant, verifier)
1371
- challenge = grant[oauth_grants_code_challenge_column]
1372
-
1373
- case grant[oauth_grants_code_challenge_method_column]
1374
- when "plain"
1375
- challenge == verifier
1376
- when "S256"
1377
- generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier))
1378
- generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=")
1379
-
1380
- challenge == generated_challenge
1381
- else
1382
- redirect_response_error("unsupported_transform_algorithm")
1383
- end
1384
- end
1385
-
1386
- # Server metadata
1387
-
1388
- def oauth_server_metadata_body(path)
1389
- issuer = base_url
1390
- issuer += "/#{path}" if path
1391
-
1392
- responses_supported = %w[code]
1393
- response_modes_supported = %w[query form_post]
1394
- grant_types_supported = %w[authorization_code]
1395
-
1396
- if use_oauth_implicit_grant_type?
1397
- responses_supported << "token"
1398
- response_modes_supported << "fragment"
1399
- grant_types_supported << "implicit"
1400
- end
1401
-
1402
- {
1403
- issuer: issuer,
1404
- authorization_endpoint: authorize_url,
1405
- token_endpoint: token_url,
1406
- registration_endpoint: oauth_applications_url,
1407
- scopes_supported: oauth_application_scopes,
1408
- response_types_supported: responses_supported,
1409
- response_modes_supported: response_modes_supported,
1410
- grant_types_supported: grant_types_supported,
1411
- token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
1412
- service_documentation: oauth_metadata_service_documentation,
1413
- ui_locales_supported: oauth_metadata_ui_locales_supported,
1414
- op_policy_uri: oauth_metadata_op_policy_uri,
1415
- op_tos_uri: oauth_metadata_op_tos_uri,
1416
- revocation_endpoint: revoke_url,
1417
- revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
1418
- introspection_endpoint: introspect_url,
1419
- introspection_endpoint_auth_methods_supported: %w[client_secret_basic],
1420
- code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
1421
- }
1422
- end
5
+ depends :oauth_base, :oauth_authorization_code_grant, :oauth_pkce, :oauth_implicit_grant,
6
+ :oauth_device_grant, :oauth_token_introspection, :oauth_token_revocation,
7
+ :oauth_application_management, :oauth_token_management
1423
8
  end
1424
9
  end