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.
- 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
|