rodauth-oauth 0.8.0 → 0.9.2

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