rodauth-oauth 0.8.0 → 0.9.2

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -3
  3. data/doc/release_notes/0_9_0.md +56 -0
  4. data/doc/release_notes/0_9_1.md +9 -0
  5. data/doc/release_notes/0_9_2.md +10 -0
  6. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +22 -1
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +8 -3
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +8 -2
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +1 -0
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +1 -0
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +1 -0
  12. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +13 -1
  13. data/lib/rodauth/features/oauth.rb +2 -2
  14. data/lib/rodauth/features/oauth_application_management.rb +23 -7
  15. data/lib/rodauth/features/oauth_assertion_base.rb +1 -1
  16. data/lib/rodauth/features/oauth_authorization_code_grant.rb +4 -1
  17. data/lib/rodauth/features/oauth_base.rb +57 -14
  18. data/lib/rodauth/features/oauth_client_credentials_grant.rb +33 -0
  19. data/lib/rodauth/features/oauth_device_grant.rb +4 -5
  20. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +252 -0
  21. data/lib/rodauth/features/oauth_jwt.rb +251 -49
  22. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +1 -0
  23. data/lib/rodauth/features/oauth_management_base.rb +72 -0
  24. data/lib/rodauth/features/oauth_pkce.rb +1 -1
  25. data/lib/rodauth/features/oauth_token_management.rb +8 -6
  26. data/lib/rodauth/features/oidc.rb +37 -7
  27. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +147 -0
  28. data/lib/rodauth/oauth/jwe_extensions.rb +64 -0
  29. data/lib/rodauth/oauth/ttl_store.rb +9 -3
  30. data/lib/rodauth/oauth/version.rb +1 -1
  31. data/locales/en.yml +6 -1
  32. data/templates/authorize.str +50 -1
  33. data/templates/jwks_field.str +4 -0
  34. data/templates/jwt_public_key_field.str +1 -1
  35. data/templates/new_oauth_application.str +1 -1
  36. data/templates/oauth_application.str +1 -1
  37. data/templates/oauth_application_oauth_tokens.str +1 -0
  38. data/templates/oauth_applications.str +1 -0
  39. data/templates/oauth_tokens.str +1 -0
  40. data/templates/scope_field.str +3 -2
  41. metadata +14 -3
  42. data/templates/jws_jwk_field.str +0 -4
@@ -1,5 +1,6 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require "rodauth/oauth/version"
3
4
  require "rodauth/oauth/ttl_store"
4
5
 
5
6
  module Rodauth
@@ -10,22 +11,41 @@ module Rodauth
10
11
 
11
12
  # Recommended to have hmac_secret as well
12
13
 
13
- auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
14
+ auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
14
15
  auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
15
16
 
16
17
  auth_value_method :oauth_jwt_token_issuer, nil
17
18
 
18
- auth_value_method :oauth_applications_jws_jwk_column, :jws_jwk
19
+ configuration_module_eval do
20
+ define_method :oauth_applications_jws_jwk_column do
21
+ warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_column`"
22
+ oauth_applications_jwks_column
23
+ end
24
+ define_method :oauth_applications_jws_jwk_label do
25
+ warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_label`"
26
+ oauth_applications_jws_jwk_label
27
+ end
28
+ define_method :oauth_application_jws_jwk_param do
29
+ warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_param`"
30
+ oauth_applications_jwks_param
31
+ end
32
+ end
33
+
34
+ auth_value_method :oauth_applications_subject_type_column, :subject_type
19
35
  auth_value_method :oauth_applications_jwt_public_key_column, :jwt_public_key
36
+ auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
37
+ auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
38
+ auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
20
39
 
21
- translatable_method :oauth_applications_jws_jwk_label, "JSON Web Keys"
22
40
  translatable_method :oauth_applications_jwt_public_key_label, "Public key"
23
- auth_value_method :oauth_application_jws_jwk_param, :jws_jwk
24
- auth_value_method :oauth_application_jwt_public_key_param, :jwt_public_key
25
41
 
42
+ auth_value_method :oauth_application_jwt_public_key_param, "jwt_public_key"
43
+ auth_value_method :oauth_application_jwks_param, "jwks"
44
+
45
+ auth_value_method :oauth_jwt_keys, {}
26
46
  auth_value_method :oauth_jwt_key, nil
27
47
  auth_value_method :oauth_jwt_public_key, nil
28
- auth_value_method :oauth_jwt_algorithm, "HS256"
48
+ auth_value_method :oauth_jwt_algorithm, "RS256"
29
49
 
30
50
  auth_value_method :oauth_jwt_jwe_key, nil
31
51
  auth_value_method :oauth_jwt_jwe_public_key, nil
@@ -119,13 +139,19 @@ module Rodauth
119
139
 
120
140
  return super unless request_object && oauth_application
121
141
 
122
- jws_jwk = if (jwk = oauth_application[oauth_applications_jws_jwk_column])
123
- jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
124
- else
125
- redirect_response_error("invalid_request_object")
126
- end
142
+ if (jwks = oauth_application_jwks)
143
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
144
+ else
145
+ redirect_response_error("invalid_request_object")
146
+ end
147
+
148
+ request_sig_enc_opts = {
149
+ jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
150
+ jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
151
+ jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
152
+ }.compact
127
153
 
128
- claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg], verify_jti: false)
154
+ claims = jwt_decode(request_object, jwks: jwks, verify_jti: false, **request_sig_enc_opts)
129
155
 
130
156
  redirect_response_error("invalid_request_object") unless claims
131
157
 
@@ -208,7 +234,12 @@ module Rodauth
208
234
  end
209
235
 
210
236
  def jwt_subject(oauth_token)
211
- case oauth_jwt_subject_type
237
+ subject_type = if oauth_application
238
+ oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
239
+ else
240
+ oauth_jwt_subject_type
241
+ end
242
+ case subject_type
212
243
  when "public"
213
244
  oauth_token[oauth_tokens_account_id_column]
214
245
  when "pairwise"
@@ -216,7 +247,7 @@ module Rodauth
216
247
  application_id = oauth_token[oauth_tokens_oauth_application_id_column]
217
248
  Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
218
249
  else
219
- raise StandardError, "unexpected subject (#{oauth_jwt_subject_type})"
250
+ raise StandardError, "unexpected subject (#{subject_type})"
220
251
  end
221
252
  end
222
253
 
@@ -245,11 +276,11 @@ module Rodauth
245
276
  }
246
277
  end
247
278
 
248
- def oauth_server_metadata_body(path)
279
+ def oauth_server_metadata_body(path = nil)
249
280
  metadata = super
250
281
  metadata.merge! \
251
282
  jwks_uri: jwks_url,
252
- token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
283
+ token_endpoint_auth_signing_alg_values_supported: (oauth_jwt_keys.keys + [oauth_jwt_algorithm]).uniq
253
284
  metadata
254
285
  end
255
286
 
@@ -257,9 +288,9 @@ module Rodauth
257
288
  @_jwt_key ||= oauth_jwt_key || begin
258
289
  if oauth_application
259
290
 
260
- if (jwk = oauth_application[oauth_applications_jws_jwk_column])
261
- jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
262
- jwk
291
+ if (jwks = oauth_application_jwks)
292
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks && jwks.is_a?(String)
293
+ jwks
263
294
  else
264
295
  oauth_application[oauth_applications_jwt_public_key_column]
265
296
  end
@@ -267,6 +298,16 @@ module Rodauth
267
298
  end
268
299
  end
269
300
 
301
+ def _jwt_public_key
302
+ @_jwt_public_key ||= oauth_jwt_public_key || begin
303
+ if oauth_application
304
+ jwks || oauth_application[oauth_applications_jwt_public_key_column]
305
+ else
306
+ _jwt_key
307
+ end
308
+ end
309
+ end
310
+
270
311
  # Resource Server only!
271
312
  #
272
313
  # returns the jwks set from the authorization server.
@@ -319,44 +360,121 @@ module Rodauth
319
360
  expected_aud == aud
320
361
  end
321
362
 
363
+ def oauth_application_jwks
364
+ jwks = oauth_application[oauth_applications_jwks_column]
365
+
366
+ if jwks
367
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
368
+ return jwks
369
+ end
370
+
371
+ jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
372
+
373
+ return unless jwks_uri
374
+
375
+ jwks_uri = URI(jwks_uri)
376
+
377
+ jwks = JWKS[jwks_uri]
378
+
379
+ return jwks if jwks
380
+
381
+ JWKS.set(jwks_uri) do
382
+ http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
383
+ http.use_ssl = jwks_uri.scheme == "https"
384
+
385
+ request = Net::HTTP::Get.new(jwks_uri.request_uri)
386
+ request["accept"] = json_response_content_type
387
+ response = http.request(request)
388
+ return unless response.code.to_i == 200
389
+
390
+ # time-to-live
391
+ ttl = if response.key?("cache-control")
392
+ cache_control = response["cache-control"]
393
+ cache_control[/max-age=(\d+)/, 1].to_i
394
+ elsif response.key?("expires")
395
+ Time.parse(response["expires"]).to_i - Time.now.to_i
396
+ end
397
+
398
+ [JSON.parse(response.body, symbolize_names: true), ttl]
399
+ end
400
+ end
401
+
322
402
  if defined?(JSON::JWT)
403
+ # json-jwt
404
+
405
+ auth_value_method :oauth_jwt_algorithms_supported, %w[
406
+ HS256 HS384 HS512
407
+ RS256 RS384 RS512
408
+ PS256 PS384 PS512
409
+ ES256 ES384 ES512 ES256K
410
+ ]
411
+ auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
412
+ RSA1_5 RSA-OAEP dir A128KW A256KW
413
+ ]
414
+ auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
415
+ A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
416
+ ]
323
417
 
324
418
  def jwk_import(data)
325
419
  JSON::JWK.new(data)
326
420
  end
327
421
 
328
- # json-jwt
329
- def jwt_encode(payload)
422
+ def jwt_encode(payload,
423
+ jwks: nil,
424
+ jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
425
+ signing_algorithm: oauth_jwt_algorithm,
426
+ encryption_algorithm: oauth_jwt_jwe_algorithm,
427
+ encryption_method: oauth_jwt_jwe_encryption_method)
330
428
  payload[:jti] = generate_jti(payload)
331
429
  jwt = JSON::JWT.new(payload)
332
- jwk = JSON::JWK.new(_jwt_key)
333
430
 
334
- jwt = jwt.sign(jwk, oauth_jwt_algorithm)
431
+ key = oauth_jwt_keys[signing_algorithm] || _jwt_key
432
+ key = key.first if key.is_a?(Array)
433
+
434
+ jwk = JSON::JWK.new(key || "")
435
+
436
+ jwt = jwt.sign(jwk, signing_algorithm)
335
437
  jwt.kid = jwk.thumbprint
336
438
 
337
- if oauth_jwt_jwe_key
338
- algorithm = oauth_jwt_jwe_algorithm.to_sym if oauth_jwt_jwe_algorithm
339
- jwt = jwt.encrypt(oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
340
- algorithm,
341
- oauth_jwt_jwe_encryption_method.to_sym)
439
+ if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
440
+ jwk = JSON::JWK.new(jwk)
441
+ jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
442
+ jwe.to_s
443
+ elsif jwe_key
444
+ algorithm = encryption_algorithm.to_sym if encryption_algorithm
445
+ meth = encryption_method.to_sym if encryption_method
446
+ jwt.encrypt(jwe_key, algorithm, meth)
447
+ else
448
+ jwt.to_s
342
449
  end
343
- jwt.to_s
344
450
  end
345
451
 
346
452
  def jwt_decode(
347
453
  token,
454
+ jwks: nil,
348
455
  jws_key: oauth_jwt_public_key || _jwt_key,
456
+ jws_algorithm: oauth_jwt_algorithm,
457
+ jwe_key: oauth_jwt_jwe_key,
458
+ jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
459
+ jws_encryption_method: oauth_jwt_jwe_encryption_method,
349
460
  verify_claims: true,
350
461
  verify_jti: true,
351
462
  verify_iss: true,
352
463
  verify_aud: false,
353
464
  **
354
465
  )
355
- token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
466
+ token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if jwe_key
356
467
 
357
468
  claims = if is_authorization_server?
358
469
  if oauth_jwt_legacy_public_key
359
470
  JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
471
+ elsif jwks
472
+ enc_algs = [jws_encryption_algorithm].compact
473
+ enc_meths = [jws_encryption_method].compact
474
+ sig_algs = [jws_algorithm].compact.map(&:to_sym)
475
+ jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
476
+ jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
477
+ jws
360
478
  elsif jws_key
361
479
  JSON::JWT.decode(token, jws_key)
362
480
  end
@@ -390,20 +508,43 @@ module Rodauth
390
508
  end
391
509
 
392
510
  elsif defined?(JWT)
393
-
394
511
  # ruby-jwt
512
+ require "rodauth/oauth/jwe_extensions" if defined?(JWE)
513
+
514
+ auth_value_method :oauth_jwt_algorithms_supported, %w[
515
+ HS256 HS384 HS512 HS512256
516
+ RS256 RS384 RS512
517
+ ED25519
518
+ ES256 ES384 ES512
519
+ PS256 PS384 PS512
520
+ ]
521
+
522
+ auth_value_methods(
523
+ :oauth_jwt_jwe_algorithms_supported,
524
+ :oauth_jwt_jwe_encryption_methods_supported
525
+ )
526
+
527
+ def oauth_jwt_jwe_algorithms_supported
528
+ JWE::VALID_ALG
529
+ end
530
+
531
+ def oauth_jwt_jwe_encryption_methods_supported
532
+ JWE::VALID_ENC
533
+ end
395
534
 
396
535
  def jwk_import(data)
397
536
  JWT::JWK.import(data).keypair
398
537
  end
399
538
 
400
- def jwt_encode(payload)
539
+ def jwt_encode(payload, signing_algorithm: oauth_jwt_algorithm)
401
540
  headers = {}
402
541
 
403
- key = _jwt_key
542
+ key = oauth_jwt_keys[signing_algorithm] || _jwt_key
543
+ key = key.first if key.is_a?(Array)
404
544
 
405
- if key.is_a?(OpenSSL::PKey::RSA)
406
- jwk = JWT::JWK.new(_jwt_key)
545
+ case key
546
+ when OpenSSL::PKey::PKey
547
+ jwk = JWT::JWK.new(key)
407
548
  headers[:kid] = jwk.kid
408
549
 
409
550
  key = jwk.keypair
@@ -411,23 +552,44 @@ module Rodauth
411
552
 
412
553
  # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
413
554
  payload[:jti] = generate_jti(payload)
414
- token = JWT.encode(payload, key, oauth_jwt_algorithm, headers)
415
-
416
- if oauth_jwt_jwe_key
417
- params = {
418
- zip: "DEF",
419
- copyright: oauth_jwt_jwe_copyright
420
- }
421
- params[:enc] = oauth_jwt_jwe_encryption_method if oauth_jwt_jwe_encryption_method
422
- params[:alg] = oauth_jwt_jwe_algorithm if oauth_jwt_jwe_algorithm
423
- token = JWE.encrypt(token, oauth_jwt_jwe_public_key || oauth_jwt_jwe_key, **params)
555
+ JWT.encode(payload, key, signing_algorithm, headers)
556
+ end
557
+
558
+ if defined?(JWE)
559
+ def jwt_encode_with_jwe(
560
+ payload,
561
+ jwks: nil,
562
+ jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
563
+ encryption_algorithm: oauth_jwt_jwe_algorithm,
564
+ encryption_method: oauth_jwt_jwe_encryption_method, **args
565
+ )
566
+
567
+ token = jwt_encode_without_jwe(payload, **args)
568
+
569
+ return token unless encryption_algorithm && encryption_method
570
+
571
+ if jwks && jwks.any? { |k| k[:use] == "enc" }
572
+ JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
573
+ elsif jwe_key
574
+ params = {
575
+ zip: "DEF",
576
+ copyright: oauth_jwt_jwe_copyright
577
+ }
578
+ params[:enc] = encryption_method if encryption_method
579
+ params[:alg] = encryption_algorithm if encryption_algorithm
580
+ JWE.encrypt(token, jwe_key, **params)
581
+ else
582
+ token
583
+ end
424
584
  end
425
585
 
426
- token
586
+ alias_method :jwt_encode_without_jwe, :jwt_encode
587
+ alias_method :jwt_encode, :jwt_encode_with_jwe
427
588
  end
428
589
 
429
590
  def jwt_decode(
430
591
  token,
592
+ jwks: nil,
431
593
  jws_key: oauth_jwt_public_key || _jwt_key,
432
594
  jws_algorithm: oauth_jwt_algorithm,
433
595
  verify_claims: true,
@@ -435,9 +597,6 @@ module Rodauth
435
597
  verify_iss: true,
436
598
  verify_aud: false
437
599
  )
438
- # decrypt jwe
439
- token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
440
-
441
600
  # verifying the JWT implies verifying:
442
601
  #
443
602
  # issuer: check that server generated the token
@@ -465,6 +624,8 @@ module Rodauth
465
624
  if oauth_jwt_legacy_public_key
466
625
  algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
467
626
  JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
627
+ elsif jwks
628
+ JWT.decode(token, nil, true, algorithms: [jws_algorithm], jwks: { keys: jwks }, **verify_claims_params).first
468
629
  elsif jws_key
469
630
  JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
470
631
  end
@@ -480,6 +641,33 @@ module Rodauth
480
641
  nil
481
642
  end
482
643
 
644
+ if defined?(JWE)
645
+ def jwt_decode_with_jwe(
646
+ token,
647
+ jwks: nil,
648
+ jwe_key: oauth_jwt_jwe_key,
649
+ jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
650
+ jws_encryption_method: oauth_jwt_jwe_encryption_method,
651
+ **args
652
+ )
653
+
654
+ token = if jwks && jwks.any? { |k| k[:use] == "enc" }
655
+ JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
656
+ elsif jwe_key
657
+ JWE.decrypt(token, jwe_key)
658
+ else
659
+ token
660
+ end
661
+
662
+ jwt_decode_without_jwe(token, jwks: jwks, **args)
663
+ rescue JWE::DecodeError => e
664
+ jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
665
+ end
666
+
667
+ alias_method :jwt_decode_without_jwe, :jwt_decode
668
+ alias_method :jwt_decode, :jwt_decode_with_jwe
669
+ end
670
+
483
671
  def jwks_set
484
672
  @jwks_set ||= [
485
673
  (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
@@ -518,5 +706,19 @@ module Rodauth
518
706
 
519
707
  super
520
708
  end
709
+
710
+ def jwt_response_success(jwt, cache = false)
711
+ response.status = 200
712
+ response["Content-Type"] ||= "application/jwt"
713
+ if cache
714
+ # defaulting to 1-day for everyone, for now at least
715
+ max_age = 60 * 60 * 24
716
+ response["Cache-Control"] = "private, max-age=#{max_age}"
717
+ else
718
+ response["Cache-Control"] = "no-store"
719
+ response["Pragma"] = "no-cache"
720
+ end
721
+ return_response(jwt)
722
+ end
521
723
  end
522
724
  end
@@ -1,5 +1,6 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require "rodauth/oauth/version"
3
4
  require "rodauth/oauth/ttl_store"
4
5
 
5
6
  module Rodauth
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:oauth_management_base, :OauthManagementBase) do
5
+ depends :oauth_base
6
+
7
+ button "Previous", "oauth_management_pagination_previous"
8
+ button "Next", "oauth_management_pagination_next"
9
+
10
+ def oauth_management_pagination_links(paginated_ds)
11
+ html = +'<nav aria-label="Pagination"><ul class="pagination">'
12
+ html << oauth_management_pagination_link(paginated_ds.prev_page, label: oauth_management_pagination_previous_button)
13
+ html << oauth_management_pagination_link(paginated_ds.current_page - 1) unless paginated_ds.first_page?
14
+ html << oauth_management_pagination_link(paginated_ds.current_page, label: paginated_ds.current_page, current: true)
15
+ html << oauth_management_pagination_link(paginated_ds.current_page + 1) unless paginated_ds.last_page?
16
+ html << oauth_management_pagination_link(paginated_ds.next_page, label: oauth_management_pagination_next_button)
17
+ html << "</ul></nav>"
18
+ end
19
+
20
+ def oauth_management_pagination_link(page, label: page, current: false, classes: "")
21
+ classes += " disabled" if current || !page
22
+ classes += " active" if current
23
+ if page
24
+ params = request.GET.merge("page" => page).map do |k, v|
25
+ v ? "#{CGI.escape(String(k))}=#{CGI.escape(String(v))}" : CGI.escape(String(k))
26
+ end.join("&")
27
+
28
+ href = "#{request.path}?#{params}"
29
+
30
+ <<-HTML
31
+ <li class="page-item #{classes}" #{'aria-current="page"' if current}>
32
+ <a class="page-link" href="#{href}" tabindex="-1" aria-disabled="#{current || !page}">
33
+ #{label}
34
+ </a>
35
+ </li>
36
+ HTML
37
+ else
38
+ <<-HTML
39
+ <li class="page-item #{classes}">
40
+ <span class="page-link">
41
+ #{label}
42
+ #{'<span class="sr-only">(current)</span>' if current}
43
+ </span>
44
+ </li>
45
+ HTML
46
+ end
47
+ end
48
+
49
+ def post_configure
50
+ super
51
+
52
+ # TODO: remove this in v1, when resource-server mode does not load all of the provider features.
53
+ return unless db
54
+
55
+ db.extension :pagination
56
+ end
57
+
58
+ private
59
+
60
+ def per_page_param(default_per_page)
61
+ per_page = param_or_nil("per_page")
62
+
63
+ return default_per_page unless per_page
64
+
65
+ per_page = per_page.to_i
66
+
67
+ return default_per_page if per_page <= 0
68
+
69
+ [per_page, default_per_page].min
70
+ end
71
+ end
72
+ end
@@ -23,7 +23,7 @@ module Rodauth
23
23
 
24
24
  private
25
25
 
26
- def authorized_oauth_application?(oauth_application, client_secret)
26
+ def authorized_oauth_application?(oauth_application, client_secret, _)
27
27
  return true if use_oauth_pkce? && param_or_nil("code_verifier")
28
28
 
29
29
  super
@@ -4,7 +4,7 @@ module Rodauth
4
4
  Feature.define(:oauth_token_management, :OauthTokenManagement) do
5
5
  using RegexpExtensions
6
6
 
7
- depends :oauth_base
7
+ depends :oauth_management_base
8
8
 
9
9
  view "oauth_tokens", "My Oauth Tokens", "oauth_tokens"
10
10
 
@@ -18,6 +18,7 @@ module Rodauth
18
18
 
19
19
  auth_value_method :oauth_tokens_route, "oauth-tokens"
20
20
  auth_value_method :oauth_tokens_id_pattern, Integer
21
+ auth_value_method :oauth_tokens_per_page, 20
21
22
 
22
23
  auth_value_methods(
23
24
  :oauth_token_path
@@ -40,12 +41,17 @@ module Rodauth
40
41
  require_account
41
42
 
42
43
  request.get do
44
+ page = Integer(param_or_nil("page") || 1)
45
+ per_page = per_page_param(oauth_tokens_per_page)
46
+
43
47
  scope.instance_variable_set(:@oauth_tokens, db[oauth_tokens_table]
44
48
  .select(Sequel[oauth_tokens_table].*, Sequel[oauth_applications_table][oauth_applications_name_column])
45
49
  .join(oauth_applications_table, Sequel[oauth_tokens_table][oauth_tokens_oauth_application_id_column] =>
46
50
  Sequel[oauth_applications_table][oauth_applications_id_column])
47
51
  .where(Sequel[oauth_tokens_table][oauth_tokens_account_id_column] => account_id)
48
- .where(oauth_tokens_revoked_at_column => nil))
52
+ .where(oauth_tokens_revoked_at_column => nil)
53
+ .order(Sequel.desc(oauth_tokens_id_column))
54
+ .paginate(page, per_page))
49
55
  oauth_tokens_view
50
56
  end
51
57
 
@@ -69,9 +75,5 @@ module Rodauth
69
75
  super
70
76
  end
71
77
  end
72
-
73
- def check_valid_uri?(uri)
74
- URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
75
- end
76
78
  end
77
79
  end
@@ -65,6 +65,13 @@ module Rodauth
65
65
  auth_value_method :oauth_application_default_scope, "openid"
66
66
  auth_value_method :oauth_application_scopes, %w[openid]
67
67
 
68
+ auth_value_method :oauth_applications_id_token_signed_response_alg_column, :id_token_signed_response_alg
69
+ auth_value_method :oauth_applications_id_token_encrypted_response_alg_column, :id_token_encrypted_response_alg
70
+ auth_value_method :oauth_applications_id_token_encrypted_response_enc_column, :id_token_encrypted_response_enc
71
+ auth_value_method :oauth_applications_userinfo_signed_response_alg_column, :userinfo_signed_response_alg
72
+ auth_value_method :oauth_applications_userinfo_encrypted_response_alg_column, :userinfo_encrypted_response_alg
73
+ auth_value_method :oauth_applications_userinfo_encrypted_response_enc_column, :userinfo_encrypted_response_enc
74
+
68
75
  auth_value_method :oauth_grants_nonce_column, :nonce
69
76
  auth_value_method :oauth_tokens_nonce_column, :nonce
70
77
 
@@ -106,7 +113,24 @@ module Rodauth
106
113
 
107
114
  fill_with_account_claims(oidc_claims, account, oauth_scopes)
108
115
 
109
- json_response_success(oidc_claims)
116
+ @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => oauth_token["client_id"]).first
117
+
118
+ if (algo = @oauth_application && @oauth_application[oauth_applications_userinfo_signed_response_alg_column])
119
+ params = {
120
+ jwks: oauth_application_jwks,
121
+ encryption_algorithm: @oauth_application[oauth_applications_userinfo_encrypted_response_alg_column],
122
+ encryption_method: @oauth_application[oauth_applications_userinfo_encrypted_response_enc_column]
123
+ }.compact
124
+
125
+ jwt = jwt_encode(
126
+ oidc_claims,
127
+ signing_algorithm: algo,
128
+ **params
129
+ )
130
+ jwt_response_success(jwt)
131
+ else
132
+ json_response_success(oidc_claims)
133
+ end
110
134
  end
111
135
 
112
136
  throw_json_response_error(authorization_required_error_status, "invalid_token")
@@ -211,8 +235,7 @@ module Rodauth
211
235
  href: authorization_server_url
212
236
  }]
213
237
  })
214
- response.write(json_payload)
215
- request.halt
238
+ return_response(json_payload)
216
239
  end
217
240
  end
218
241
  end
@@ -293,7 +316,7 @@ module Rodauth
293
316
  def create_oauth_grant(create_params = {})
294
317
  return super unless (nonce = param_or_nil("nonce"))
295
318
 
296
- super(oauth_grants_nonce_column => nonce)
319
+ super(create_params.merge(oauth_grants_nonce_column => nonce))
297
320
  end
298
321
 
299
322
  def create_oauth_token_from_authorization_code(oauth_grant, create_params)
@@ -330,7 +353,14 @@ module Rodauth
330
353
 
331
354
  fill_with_account_claims(id_token_claims, account, oauth_scopes)
332
355
 
333
- oauth_token[:id_token] = jwt_encode(id_token_claims)
356
+ params = {
357
+ jwks: oauth_application_jwks,
358
+ signing_algorithm: oauth_application[oauth_applications_id_token_signed_response_alg_column] || oauth_jwt_algorithm,
359
+ encryption_algorithm: oauth_application[oauth_applications_id_token_encrypted_response_alg_column],
360
+ encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
361
+ }.compact
362
+
363
+ oauth_token[:id_token] = jwt_encode(id_token_claims, **params)
334
364
  end
335
365
 
336
366
  # aka fill_with_standard_claims
@@ -443,7 +473,7 @@ module Rodauth
443
473
 
444
474
  # Metadata
445
475
 
446
- def openid_configuration_body(path)
476
+ def openid_configuration_body(path = nil)
447
477
  metadata = oauth_server_metadata_body(path).select do |k, _|
448
478
  VALID_METADATA_KEYS.include?(k)
449
479
  end
@@ -504,7 +534,7 @@ module Rodauth
504
534
  response["Access-Control-Allow-Methods"] = "GET, OPTIONS"
505
535
  response["Access-Control-Max-Age"] = "3600"
506
536
  response.status = 200
507
- request.halt
537
+ return_response
508
538
  end
509
539
  end
510
540
  end