rodauth-oauth 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -424
  3. data/README.md +26 -389
  4. data/doc/release_notes/0_0_1.md +3 -0
  5. data/doc/release_notes/0_0_2.md +15 -0
  6. data/doc/release_notes/0_0_3.md +31 -0
  7. data/doc/release_notes/0_0_4.md +36 -0
  8. data/doc/release_notes/0_0_5.md +36 -0
  9. data/doc/release_notes/0_0_6.md +21 -0
  10. data/doc/release_notes/0_1_0.md +44 -0
  11. data/doc/release_notes/0_2_0.md +43 -0
  12. data/doc/release_notes/0_3_0.md +28 -0
  13. data/doc/release_notes/0_4_0.md +18 -0
  14. data/doc/release_notes/0_4_1.md +9 -0
  15. data/doc/release_notes/0_4_2.md +5 -0
  16. data/doc/release_notes/0_4_3.md +3 -0
  17. data/doc/release_notes/0_5_0.md +11 -0
  18. data/doc/release_notes/0_5_1.md +13 -0
  19. data/doc/release_notes/0_6_0.md +9 -0
  20. data/doc/release_notes/0_6_1.md +6 -0
  21. data/doc/release_notes/0_7_0.md +20 -0
  22. data/doc/release_notes/0_7_1.md +10 -0
  23. data/doc/release_notes/0_7_2.md +21 -0
  24. data/doc/release_notes/0_7_3.md +10 -0
  25. data/doc/release_notes/0_7_4.md +5 -0
  26. data/doc/release_notes/0_8_0.md +37 -0
  27. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +3 -3
  28. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +11 -0
  29. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +20 -0
  30. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +22 -10
  31. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +11 -5
  32. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +38 -0
  33. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +5 -5
  34. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +11 -15
  35. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +9 -1
  36. data/lib/rodauth/features/oauth.rb +3 -1418
  37. data/lib/rodauth/features/oauth_application_management.rb +209 -0
  38. data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
  39. data/lib/rodauth/features/oauth_authorization_code_grant.rb +249 -0
  40. data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
  41. data/lib/rodauth/features/oauth_base.rb +735 -0
  42. data/lib/rodauth/features/oauth_device_grant.rb +221 -0
  43. data/lib/rodauth/features/oauth_http_mac.rb +3 -21
  44. data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
  45. data/lib/rodauth/features/oauth_jwt.rb +37 -60
  46. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
  47. data/lib/rodauth/features/oauth_pkce.rb +98 -0
  48. data/lib/rodauth/features/oauth_resource_server.rb +21 -0
  49. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
  50. data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
  51. data/lib/rodauth/features/oauth_token_management.rb +77 -0
  52. data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
  53. data/lib/rodauth/features/oidc.rb +4 -3
  54. data/lib/rodauth/oauth/database_extensions.rb +15 -2
  55. data/lib/rodauth/oauth/refinements.rb +48 -0
  56. data/lib/rodauth/oauth/version.rb +1 -1
  57. data/locales/en.yml +28 -12
  58. data/templates/authorize.str +7 -7
  59. data/templates/client_secret_field.str +2 -2
  60. data/templates/description_field.str +1 -1
  61. data/templates/device_search.str +11 -0
  62. data/templates/device_verification.str +24 -0
  63. data/templates/homepage_url_field.str +2 -2
  64. data/templates/jws_jwk_field.str +4 -0
  65. data/templates/jwt_public_key_field.str +4 -0
  66. data/templates/name_field.str +1 -1
  67. data/templates/new_oauth_application.str +9 -0
  68. data/templates/oauth_application.str +7 -3
  69. data/templates/oauth_application_oauth_tokens.str +51 -0
  70. data/templates/oauth_applications.str +2 -2
  71. data/templates/oauth_tokens.str +9 -11
  72. data/templates/redirect_uri_field.str +2 -2
  73. metadata +71 -3
  74. data/lib/rodauth/features/oauth_saml.rb +0 -104
@@ -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