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.
- checksums.yaml +4 -4
- data/README.md +6 -3
- data/doc/release_notes/0_9_0.md +56 -0
- data/doc/release_notes/0_9_1.md +9 -0
- data/doc/release_notes/0_9_2.md +10 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +22 -1
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +8 -3
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +8 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +1 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +1 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +1 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +13 -1
- data/lib/rodauth/features/oauth.rb +2 -2
- data/lib/rodauth/features/oauth_application_management.rb +23 -7
- data/lib/rodauth/features/oauth_assertion_base.rb +1 -1
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +4 -1
- data/lib/rodauth/features/oauth_base.rb +57 -14
- data/lib/rodauth/features/oauth_client_credentials_grant.rb +33 -0
- data/lib/rodauth/features/oauth_device_grant.rb +4 -5
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +252 -0
- data/lib/rodauth/features/oauth_jwt.rb +251 -49
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +1 -0
- data/lib/rodauth/features/oauth_management_base.rb +72 -0
- data/lib/rodauth/features/oauth_pkce.rb +1 -1
- data/lib/rodauth/features/oauth_token_management.rb +8 -6
- data/lib/rodauth/features/oidc.rb +37 -7
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +147 -0
- data/lib/rodauth/oauth/jwe_extensions.rb +64 -0
- data/lib/rodauth/oauth/ttl_store.rb +9 -3
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +6 -1
- data/templates/authorize.str +50 -1
- data/templates/jwks_field.str +4 -0
- data/templates/jwt_public_key_field.str +1 -1
- data/templates/new_oauth_application.str +1 -1
- data/templates/oauth_application.str +1 -1
- data/templates/oauth_application_oauth_tokens.str +1 -0
- data/templates/oauth_applications.str +1 -0
- data/templates/oauth_tokens.str +1 -0
- data/templates/scope_field.str +3 -2
- metadata +14 -3
- 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
|
-
|
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, "
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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,
|
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
|
-
|
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 (#{
|
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 (
|
261
|
-
|
262
|
-
|
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
|
-
|
329
|
-
|
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
|
-
|
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
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
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
|
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
|
-
|
406
|
-
|
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
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
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
|
-
|
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
|
@@ -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
|
@@ -4,7 +4,7 @@ module Rodauth
|
|
4
4
|
Feature.define(:oauth_token_management, :OauthTokenManagement) do
|
5
5
|
using RegexpExtensions
|
6
6
|
|
7
|
-
depends :
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
537
|
+
return_response
|
508
538
|
end
|
509
539
|
end
|
510
540
|
end
|