rodauth-oauth 0.7.3 → 0.9.0

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