rodauth-oauth 0.7.3 → 0.9.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -418
  3. data/README.md +30 -390
  4. data/doc/release_notes/0_0_1.md +3 -0
  5. data/doc/release_notes/0_0_2.md +15 -0
  6. data/doc/release_notes/0_0_3.md +31 -0
  7. data/doc/release_notes/0_0_4.md +36 -0
  8. data/doc/release_notes/0_0_5.md +36 -0
  9. data/doc/release_notes/0_0_6.md +21 -0
  10. data/doc/release_notes/0_1_0.md +44 -0
  11. data/doc/release_notes/0_2_0.md +43 -0
  12. data/doc/release_notes/0_3_0.md +28 -0
  13. data/doc/release_notes/0_4_0.md +18 -0
  14. data/doc/release_notes/0_4_1.md +9 -0
  15. data/doc/release_notes/0_4_2.md +5 -0
  16. data/doc/release_notes/0_4_3.md +3 -0
  17. data/doc/release_notes/0_5_0.md +11 -0
  18. data/doc/release_notes/0_5_1.md +13 -0
  19. data/doc/release_notes/0_6_0.md +9 -0
  20. data/doc/release_notes/0_6_1.md +6 -0
  21. data/doc/release_notes/0_7_0.md +20 -0
  22. data/doc/release_notes/0_7_1.md +10 -0
  23. data/doc/release_notes/0_7_2.md +21 -0
  24. data/doc/release_notes/0_7_3.md +10 -0
  25. data/doc/release_notes/0_7_4.md +5 -0
  26. data/doc/release_notes/0_8_0.md +37 -0
  27. data/doc/release_notes/0_9_0.md +56 -0
  28. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +50 -0
  29. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +11 -0
  30. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +20 -0
  31. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +55 -0
  32. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +29 -0
  33. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +39 -0
  34. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +30 -0
  35. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +35 -0
  36. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +21 -1
  37. data/lib/rodauth/features/oauth.rb +3 -1418
  38. data/lib/rodauth/features/oauth_application_management.rb +225 -0
  39. data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
  40. data/lib/rodauth/features/oauth_authorization_code_grant.rb +252 -0
  41. data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
  42. data/lib/rodauth/features/oauth_base.rb +771 -0
  43. data/lib/rodauth/features/oauth_client_credentials_grant.rb +33 -0
  44. data/lib/rodauth/features/oauth_device_grant.rb +220 -0
  45. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +252 -0
  46. data/lib/rodauth/features/oauth_http_mac.rb +3 -21
  47. data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
  48. data/lib/rodauth/features/oauth_jwt.rb +276 -100
  49. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
  50. data/lib/rodauth/features/oauth_management_base.rb +68 -0
  51. data/lib/rodauth/features/oauth_pkce.rb +98 -0
  52. data/lib/rodauth/features/oauth_resource_server.rb +21 -0
  53. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
  54. data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
  55. data/lib/rodauth/features/oauth_token_management.rb +79 -0
  56. data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
  57. data/lib/rodauth/features/oidc.rb +36 -6
  58. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +147 -0
  59. data/lib/rodauth/oauth/database_extensions.rb +15 -2
  60. data/lib/rodauth/oauth/jwe_extensions.rb +64 -0
  61. data/lib/rodauth/oauth/refinements.rb +48 -0
  62. data/lib/rodauth/oauth/ttl_store.rb +9 -3
  63. data/lib/rodauth/oauth/version.rb +1 -1
  64. data/locales/en.yml +33 -12
  65. data/templates/authorize.str +57 -8
  66. data/templates/client_secret_field.str +2 -2
  67. data/templates/description_field.str +1 -1
  68. data/templates/device_search.str +11 -0
  69. data/templates/device_verification.str +24 -0
  70. data/templates/homepage_url_field.str +2 -2
  71. data/templates/jwks_field.str +4 -0
  72. data/templates/jwt_public_key_field.str +4 -0
  73. data/templates/name_field.str +1 -1
  74. data/templates/new_oauth_application.str +9 -0
  75. data/templates/oauth_application.str +7 -3
  76. data/templates/oauth_application_oauth_tokens.str +52 -0
  77. data/templates/oauth_applications.str +3 -2
  78. data/templates/oauth_tokens.str +10 -11
  79. data/templates/redirect_uri_field.str +2 -2
  80. metadata +84 -4
  81. data/lib/rodauth/features/oauth_saml.rb +0 -104
@@ -10,16 +10,38 @@ module Rodauth
10
10
 
11
11
  # Recommended to have hmac_secret as well
12
12
 
13
- auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
13
+ auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
14
14
  auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
15
15
 
16
16
  auth_value_method :oauth_jwt_token_issuer, nil
17
17
 
18
- auth_value_method :oauth_application_jws_jwk_column, nil
18
+ configuration_module_eval do
19
+ define_method :oauth_applications_jws_jwk_column do
20
+ warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_column`"
21
+ oauth_applications_jwks_column
22
+ end
23
+ define_method :oauth_applications_jws_jwk_label do
24
+ warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_label`"
25
+ oauth_applications_jws_jwk_label
26
+ end
27
+ define_method :oauth_application_jws_jwk_param do
28
+ warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_param`"
29
+ oauth_applications_jwks_param
30
+ end
31
+ end
32
+
33
+ auth_value_method :oauth_applications_subject_type_column, :subject_type
34
+ auth_value_method :oauth_applications_jwt_public_key_column, :jwt_public_key
35
+ auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
36
+ auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
37
+ auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
38
+
39
+ translatable_method :oauth_applications_jwt_public_key_label, "Public key"
19
40
 
41
+ auth_value_method :oauth_jwt_keys, {}
20
42
  auth_value_method :oauth_jwt_key, nil
21
43
  auth_value_method :oauth_jwt_public_key, nil
22
- auth_value_method :oauth_jwt_algorithm, "HS256"
44
+ auth_value_method :oauth_jwt_algorithm, "RS256"
23
45
 
24
46
  auth_value_method :oauth_jwt_jwe_key, nil
25
47
  auth_value_method :oauth_jwt_jwe_public_key, nil
@@ -113,15 +135,19 @@ module Rodauth
113
135
 
114
136
  return super unless request_object && oauth_application
115
137
 
116
- jws_jwk = if oauth_application[oauth_application_jws_jwk_column]
117
- jwk = oauth_application[oauth_application_jws_jwk_column]
138
+ if (jwks = oauth_application_jwks)
139
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
140
+ else
141
+ redirect_response_error("invalid_request_object")
142
+ end
118
143
 
119
- jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
120
- else
121
- redirect_response_error("invalid_request_object")
122
- end
144
+ request_sig_enc_opts = {
145
+ jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
146
+ jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
147
+ jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
148
+ }.compact
123
149
 
124
- claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg], verify_jti: false)
150
+ claims = jwt_decode(request_object, jwks: jwks, verify_jti: false, **request_sig_enc_opts)
125
151
 
126
152
  redirect_response_error("invalid_request_object") unless claims
127
153
 
@@ -145,51 +171,6 @@ module Rodauth
145
171
 
146
172
  # /token
147
173
 
148
- def require_oauth_application
149
- # requset authentication optional for assertions
150
- return super unless param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
151
-
152
- claims = jwt_decode(param("assertion"))
153
-
154
- redirect_response_error("invalid_grant") unless claims
155
-
156
- @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
157
-
158
- authorization_required unless @oauth_application
159
- end
160
-
161
- def validate_oauth_token_params
162
- if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
163
- redirect_response_error("invalid_client") unless param_or_nil("assertion")
164
- else
165
- super
166
- end
167
- end
168
-
169
- def create_oauth_token
170
- if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
171
- create_oauth_token_from_assertion
172
- else
173
- super
174
- end
175
- end
176
-
177
- def create_oauth_token_from_assertion
178
- claims = jwt_decode(param("assertion"))
179
-
180
- account = account_ds(claims["sub"]).first
181
-
182
- redirect_response_error("invalid_client") unless oauth_application && account
183
-
184
- create_params = {
185
- oauth_tokens_account_id_column => claims["sub"],
186
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
187
- oauth_tokens_scopes_column => claims["scope"]
188
- }
189
-
190
- generate_oauth_token(create_params, false)
191
- end
192
-
193
174
  def generate_oauth_token(params = {}, should_generate_refresh_token = true)
194
175
  create_params = {
195
176
  oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
@@ -249,7 +230,12 @@ module Rodauth
249
230
  end
250
231
 
251
232
  def jwt_subject(oauth_token)
252
- case oauth_jwt_subject_type
233
+ subject_type = if oauth_application
234
+ oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
235
+ else
236
+ oauth_jwt_subject_type
237
+ end
238
+ case subject_type
253
239
  when "public"
254
240
  oauth_token[oauth_tokens_account_id_column]
255
241
  when "pairwise"
@@ -257,7 +243,7 @@ module Rodauth
257
243
  application_id = oauth_token[oauth_tokens_oauth_application_id_column]
258
244
  Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
259
245
  else
260
- raise StandardError, "unexpected subject (#{oauth_jwt_subject_type})"
246
+ raise StandardError, "unexpected subject (#{subject_type})"
261
247
  end
262
248
  end
263
249
 
@@ -286,16 +272,36 @@ module Rodauth
286
272
  }
287
273
  end
288
274
 
289
- def oauth_server_metadata_body(path)
275
+ def oauth_server_metadata_body(path = nil)
290
276
  metadata = super
291
277
  metadata.merge! \
292
278
  jwks_uri: jwks_url,
293
- token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
279
+ token_endpoint_auth_signing_alg_values_supported: (oauth_jwt_keys.keys + [oauth_jwt_algorithm]).uniq
294
280
  metadata
295
281
  end
296
282
 
297
283
  def _jwt_key
298
- @_jwt_key ||= oauth_jwt_key || (oauth_application[oauth_applications_client_secret_column] if oauth_application)
284
+ @_jwt_key ||= oauth_jwt_key || begin
285
+ if oauth_application
286
+
287
+ if (jwks = oauth_application_jwks)
288
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks && jwks.is_a?(String)
289
+ jwks
290
+ else
291
+ oauth_application[oauth_applications_jwt_public_key_column]
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ def _jwt_public_key
298
+ @_jwt_public_key ||= oauth_jwt_public_key || begin
299
+ if oauth_application
300
+ jwks || oauth_application[oauth_applications_jwt_public_key_column]
301
+ else
302
+ _jwt_key
303
+ end
304
+ end
299
305
  end
300
306
 
301
307
  # Resource Server only!
@@ -346,46 +352,125 @@ module Rodauth
346
352
  generate_jti(claims) == jti
347
353
  end
348
354
 
349
- def verify_aud(aud, claims)
350
- aud == (oauth_jwt_audience || claims["client_id"])
355
+ def verify_aud(expected_aud, aud)
356
+ expected_aud == aud
357
+ end
358
+
359
+ def oauth_application_jwks
360
+ jwks = oauth_application[oauth_applications_jwks_column]
361
+
362
+ if jwks
363
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
364
+ return jwks
365
+ end
366
+
367
+ jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
368
+
369
+ return unless jwks_uri
370
+
371
+ jwks_uri = URI(jwks_uri)
372
+
373
+ jwks = JWKS[jwks_uri]
374
+
375
+ return jwks if jwks
376
+
377
+ JWKS.set(jwks_uri) do
378
+ http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
379
+ http.use_ssl = jwks_uri.scheme == "https"
380
+
381
+ request = Net::HTTP::Get.new(jwks_uri.request_uri)
382
+ request["accept"] = json_response_content_type
383
+ response = http.request(request)
384
+ return unless response.code.to_i == 200
385
+
386
+ # time-to-live
387
+ ttl = if response.key?("cache-control")
388
+ cache_control = response["cache-control"]
389
+ cache_control[/max-age=(\d+)/, 1].to_i
390
+ elsif response.key?("expires")
391
+ Time.parse(response["expires"]).to_i - Time.now.to_i
392
+ end
393
+
394
+ [JSON.parse(response.body, symbolize_names: true), ttl]
395
+ end
351
396
  end
352
397
 
353
398
  if defined?(JSON::JWT)
399
+ # json-jwt
400
+
401
+ auth_value_method :oauth_jwt_algorithms_supported, %w[
402
+ HS256 HS384 HS512
403
+ RS256 RS384 RS512
404
+ PS256 PS384 PS512
405
+ ES256 ES384 ES512 ES256K
406
+ ]
407
+ auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
408
+ RSA1_5 RSA-OAEP dir A128KW A256KW
409
+ ]
410
+ auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
411
+ A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
412
+ ]
354
413
 
355
414
  def jwk_import(data)
356
415
  JSON::JWK.new(data)
357
416
  end
358
417
 
359
- # json-jwt
360
- def jwt_encode(payload)
418
+ def jwt_encode(payload,
419
+ jwks: nil,
420
+ jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
421
+ signing_algorithm: oauth_jwt_algorithm,
422
+ encryption_algorithm: oauth_jwt_jwe_algorithm,
423
+ encryption_method: oauth_jwt_jwe_encryption_method)
361
424
  payload[:jti] = generate_jti(payload)
362
425
  jwt = JSON::JWT.new(payload)
363
- jwk = JSON::JWK.new(_jwt_key)
364
426
 
365
- jwt = jwt.sign(jwk, oauth_jwt_algorithm)
427
+ key = oauth_jwt_keys[signing_algorithm] || _jwt_key
428
+ key = key.first if key.is_a?(Array)
429
+
430
+ jwk = JSON::JWK.new(key || "")
431
+
432
+ jwt = jwt.sign(jwk, signing_algorithm)
366
433
  jwt.kid = jwk.thumbprint
367
434
 
368
- if oauth_jwt_jwe_key
369
- algorithm = oauth_jwt_jwe_algorithm.to_sym if oauth_jwt_jwe_algorithm
370
- jwt = jwt.encrypt(oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
371
- algorithm,
372
- oauth_jwt_jwe_encryption_method.to_sym)
435
+ if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
436
+ jwk = JSON::JWK.new(jwk)
437
+ jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
438
+ jwe.to_s
439
+ elsif jwe_key
440
+ algorithm = encryption_algorithm.to_sym if encryption_algorithm
441
+ meth = encryption_method.to_sym if encryption_method
442
+ jwt.encrypt(jwe_key, algorithm, meth)
443
+ else
444
+ jwt.to_s
373
445
  end
374
- jwt.to_s
375
446
  end
376
447
 
377
448
  def jwt_decode(
378
449
  token,
450
+ jwks: nil,
379
451
  jws_key: oauth_jwt_public_key || _jwt_key,
452
+ jws_algorithm: oauth_jwt_algorithm,
453
+ jwe_key: oauth_jwt_jwe_key,
454
+ jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
455
+ jws_encryption_method: oauth_jwt_jwe_encryption_method,
380
456
  verify_claims: true,
381
457
  verify_jti: true,
458
+ verify_iss: true,
459
+ verify_aud: false,
382
460
  **
383
461
  )
384
- token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
462
+ token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if jwe_key
385
463
 
386
464
  claims = if is_authorization_server?
387
465
  if oauth_jwt_legacy_public_key
388
466
  JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
467
+ elsif jwks
468
+ enc_algs = [jws_encryption_algorithm].compact
469
+ enc_meths = [jws_encryption_method].compact
470
+ sig_algs = [jws_algorithm].compact.map(&:to_sym)
471
+ jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
472
+ jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
473
+ jws
389
474
  elsif jws_key
390
475
  JSON::JWT.decode(token, jws_key)
391
476
  end
@@ -393,11 +478,15 @@ module Rodauth
393
478
  JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
394
479
  end
395
480
 
396
- if verify_claims && !(claims[:iss] == issuer &&
397
- verify_aud(claims[:aud], claims) &&
398
- (!claims[:iat] || Time.at(claims[:iat]) > (Time.now - oauth_token_expires_in)) &&
399
- (!claims[:exp] || Time.at(claims[:exp]) > Time.now) &&
400
- (!verify_jti || verify_jti(claims[:jti], claims)))
481
+ now = Time.now
482
+ if verify_claims && (
483
+ (!claims[:exp] || Time.at(claims[:exp]) < now) &&
484
+ (claims[:nbf] && Time.at(claims[:nbf]) < now) &&
485
+ (claims[:iat] && Time.at(claims[:iat]) < now) &&
486
+ (verify_iss && claims[:iss] != issuer) &&
487
+ (verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
488
+ (verify_jti && !verify_jti(claims[:jti], claims))
489
+ )
401
490
  return
402
491
  end
403
492
 
@@ -415,20 +504,43 @@ module Rodauth
415
504
  end
416
505
 
417
506
  elsif defined?(JWT)
418
-
419
507
  # ruby-jwt
508
+ require "rodauth/oauth/jwe_extensions" if defined?(JWE)
509
+
510
+ auth_value_method :oauth_jwt_algorithms_supported, %w[
511
+ HS256 HS384 HS512 HS512256
512
+ RS256 RS384 RS512
513
+ ED25519
514
+ ES256 ES384 ES512
515
+ PS256 PS384 PS512
516
+ ]
517
+
518
+ auth_value_methods(
519
+ :oauth_jwt_jwe_algorithms_supported,
520
+ :oauth_jwt_jwe_encryption_methods_supported
521
+ )
522
+
523
+ def oauth_jwt_jwe_algorithms_supported
524
+ JWE::VALID_ALG
525
+ end
526
+
527
+ def oauth_jwt_jwe_encryption_methods_supported
528
+ JWE::VALID_ENC
529
+ end
420
530
 
421
531
  def jwk_import(data)
422
532
  JWT::JWK.import(data).keypair
423
533
  end
424
534
 
425
- def jwt_encode(payload)
535
+ def jwt_encode(payload, signing_algorithm: oauth_jwt_algorithm)
426
536
  headers = {}
427
537
 
428
- key = _jwt_key
538
+ key = oauth_jwt_keys[signing_algorithm] || _jwt_key
539
+ key = key.first if key.is_a?(Array)
429
540
 
430
- if key.is_a?(OpenSSL::PKey::RSA)
431
- jwk = JWT::JWK.new(_jwt_key)
541
+ case key
542
+ when OpenSSL::PKey::PKey
543
+ jwk = JWT::JWK.new(key)
432
544
  headers[:kid] = jwk.kid
433
545
 
434
546
  key = jwk.keypair
@@ -436,31 +548,51 @@ module Rodauth
436
548
 
437
549
  # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
438
550
  payload[:jti] = generate_jti(payload)
439
- token = JWT.encode(payload, key, oauth_jwt_algorithm, headers)
440
-
441
- if oauth_jwt_jwe_key
442
- params = {
443
- zip: "DEF",
444
- copyright: oauth_jwt_jwe_copyright
445
- }
446
- params[:enc] = oauth_jwt_jwe_encryption_method if oauth_jwt_jwe_encryption_method
447
- params[:alg] = oauth_jwt_jwe_algorithm if oauth_jwt_jwe_algorithm
448
- token = JWE.encrypt(token, oauth_jwt_jwe_public_key || oauth_jwt_jwe_key, **params)
551
+ JWT.encode(payload, key, signing_algorithm, headers)
552
+ end
553
+
554
+ if defined?(JWE)
555
+ def jwt_encode_with_jwe(
556
+ payload,
557
+ jwks: nil,
558
+ jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
559
+ encryption_algorithm: oauth_jwt_jwe_algorithm,
560
+ encryption_method: oauth_jwt_jwe_encryption_method, **args
561
+ )
562
+
563
+ token = jwt_encode_without_jwe(payload, **args)
564
+
565
+ return token unless encryption_algorithm && encryption_method
566
+
567
+ if jwks && jwks.any? { |k| k[:use] == "enc" }
568
+ JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
569
+ elsif jwe_key
570
+ params = {
571
+ zip: "DEF",
572
+ copyright: oauth_jwt_jwe_copyright
573
+ }
574
+ params[:enc] = encryption_method if encryption_method
575
+ params[:alg] = encryption_algorithm if encryption_algorithm
576
+ JWE.encrypt(token, jwe_key, **params)
577
+ else
578
+ token
579
+ end
449
580
  end
450
581
 
451
- token
582
+ alias_method :jwt_encode_without_jwe, :jwt_encode
583
+ alias_method :jwt_encode, :jwt_encode_with_jwe
452
584
  end
453
585
 
454
586
  def jwt_decode(
455
587
  token,
588
+ jwks: nil,
456
589
  jws_key: oauth_jwt_public_key || _jwt_key,
457
590
  jws_algorithm: oauth_jwt_algorithm,
458
591
  verify_claims: true,
459
- verify_jti: true
592
+ verify_jti: true,
593
+ verify_iss: true,
594
+ verify_aud: false
460
595
  )
461
- # decrypt jwe
462
- token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
463
-
464
596
  # verifying the JWT implies verifying:
465
597
  #
466
598
  # issuer: check that server generated the token
@@ -472,7 +604,7 @@ module Rodauth
472
604
  #
473
605
  verify_claims_params = if verify_claims
474
606
  {
475
- verify_iss: true,
607
+ verify_iss: verify_iss,
476
608
  iss: issuer,
477
609
  # can't use stock aud verification, as it's dependent on the client application id
478
610
  verify_aud: false,
@@ -488,6 +620,8 @@ module Rodauth
488
620
  if oauth_jwt_legacy_public_key
489
621
  algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
490
622
  JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
623
+ elsif jwks
624
+ JWT.decode(token, nil, true, algorithms: [jws_algorithm], jwks: { keys: jwks }, **verify_claims_params).first
491
625
  elsif jws_key
492
626
  JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
493
627
  end
@@ -496,13 +630,40 @@ module Rodauth
496
630
  JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
497
631
  end
498
632
 
499
- return if verify_claims && !verify_aud(claims["aud"], claims)
633
+ return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
500
634
 
501
635
  claims
502
636
  rescue JWT::DecodeError, JWT::JWKError
503
637
  nil
504
638
  end
505
639
 
640
+ if defined?(JWE)
641
+ def jwt_decode_with_jwe(
642
+ token,
643
+ jwks: nil,
644
+ jwe_key: oauth_jwt_jwe_key,
645
+ jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
646
+ jws_encryption_method: oauth_jwt_jwe_encryption_method,
647
+ **args
648
+ )
649
+
650
+ token = if jwks && jwks.any? { |k| k[:use] == "enc" }
651
+ JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
652
+ elsif jwe_key
653
+ JWE.decrypt(token, jwe_key)
654
+ else
655
+ token
656
+ end
657
+
658
+ jwt_decode_without_jwe(token, jwks: jwks, **args)
659
+ rescue JWE::DecodeError => e
660
+ jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
661
+ end
662
+
663
+ alias_method :jwt_decode_without_jwe, :jwt_decode
664
+ alias_method :jwt_decode, :jwt_decode_with_jwe
665
+ end
666
+
506
667
  def jwks_set
507
668
  @jwks_set ||= [
508
669
  (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
@@ -541,5 +702,20 @@ module Rodauth
541
702
 
542
703
  super
543
704
  end
705
+
706
+ def jwt_response_success(jwt, cache = false)
707
+ response.status = 200
708
+ response["Content-Type"] ||= "application/jwt"
709
+ if cache
710
+ # defaulting to 1-day for everyone, for now at least
711
+ max_age = 60 * 60 * 24
712
+ response["Cache-Control"] = "private, max-age=#{max_age}"
713
+ else
714
+ response["Cache-Control"] = "no-store"
715
+ response["Pragma"] = "no-cache"
716
+ end
717
+ response.write(jwt)
718
+ request.halt
719
+ end
544
720
  end
545
721
  end
@@ -0,0 +1,59 @@
1
+ # frozen-string-literal: true
2
+
3
+ require "rodauth/oauth/ttl_store"
4
+
5
+ module Rodauth
6
+ Feature.define(:oauth_jwt_bearer_grant, :OauthJwtBearerGrant) do
7
+ depends :oauth_assertion_base, :oauth_jwt
8
+
9
+ auth_value_methods(
10
+ :require_oauth_application_from_jwt_bearer_assertion_issuer,
11
+ :require_oauth_application_from_jwt_bearer_assertion_subject,
12
+ :account_from_jwt_bearer_assertion
13
+ )
14
+
15
+ private
16
+
17
+ def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
18
+ claims = jwt_assertion(assertion)
19
+
20
+ return unless claims
21
+
22
+ db[oauth_applications_table].where(
23
+ oauth_applications_client_id_column => claims["iss"]
24
+ ).first
25
+ end
26
+
27
+ def require_oauth_application_from_jwt_bearer_assertion_subject(assertion)
28
+ claims = jwt_assertion(assertion)
29
+
30
+ return unless claims
31
+
32
+ db[oauth_applications_table].where(
33
+ oauth_applications_client_id_column => claims["sub"]
34
+ ).first
35
+ end
36
+
37
+ def account_from_jwt_bearer_assertion(assertion)
38
+ claims = jwt_assertion(assertion)
39
+
40
+ return unless claims
41
+
42
+ account_from_bearer_assertion_subject(claims["sub"])
43
+ end
44
+
45
+ def jwt_assertion(assertion)
46
+ claims = jwt_decode(assertion, verify_iss: false, verify_aud: false)
47
+ return unless verify_aud(token_url, claims["aud"])
48
+
49
+ claims
50
+ end
51
+
52
+ def oauth_server_metadata_body(*)
53
+ super.tap do |data|
54
+ data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:jwt-bearer"
55
+ data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,68 @@
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
+ db.extension :pagination
52
+ end
53
+
54
+ private
55
+
56
+ def per_page_param(default_per_page)
57
+ per_page = param_or_nil("per_page")
58
+
59
+ return default_per_page unless per_page
60
+
61
+ per_page = per_page.to_i
62
+
63
+ return default_per_page if per_page <= 0
64
+
65
+ [per_page, default_per_page].min
66
+ end
67
+ end
68
+ end