rodauth-oauth 0.9.3 → 0.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79fa7abfcf7ea1c0a2594f92bfd7a59e53769dec5a7221a22f740b37471e6e07
4
- data.tar.gz: 9f7e8ce4370d44d985940d46d569e3192c35e53cb6e3841094517e64258b2b1a
3
+ metadata.gz: f9b68ff6e15b91128db72a07fa91b86afb70352f9582fa8c27e7abfe3c0dc17c
4
+ data.tar.gz: 1c35b67bc10619c8de31cbcef514636e7975307a0cfc02585ae10ec97de74be1
5
5
  SHA512:
6
- metadata.gz: 520a85416c418f93615a471133201c7ec6dd5c16ff9d697a6ca87b609084b58c7b9799c5d3c7a8d02a4fc68ca9750c9ef41dd07dde17c1e1153f0cec8e58f3c1
7
- data.tar.gz: 8e7ebc28ba158f43b5226a2de98fc76de8c81e8a8c87dbaa95a69050eed96793e487666389b60be6694437c298fdf23f183a1df098983eeec5f5549df0321c7c
6
+ metadata.gz: 2cf0e357529093b45834697c54bae5eaf17419885e04ccba279d18e65464aa8d8fb2e49da09dd5c96c83331e0f60915e993af5dfc7decfff4c8752b5401dfe8a
7
+ data.tar.gz: 784d5184526ff8dcbc3c112eb58311705baf82e1bf17b40b87cd55dc43f0b06cc6bf7c2cb71f67caf315008b14d3fe4b9fe8eea991a0475586bf4effb0d77ed3
data/README.md CHANGED
@@ -21,14 +21,16 @@ This gem implements the following RFCs and features of OAuth:
21
21
  * `oauth_token_introspection` - [Token introspection](https://tools.ietf.org/html/rfc7662);
22
22
  * [Authorization Server Metadata](https://tools.ietf.org/html/rfc8414);
23
23
  * `oauth_pkce` - [PKCE](https://tools.ietf.org/html/rfc7636);
24
- * Access Type (Token refresh online and offline);
25
24
  * `oauth_jwt` - [JWT Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07);
25
+ * Supports [JWT Secured Authorization Request](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
26
+ * `oauth_resource_indicators` - [Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707);
27
+ * Access Type (Token refresh online and offline);
26
28
  * `oauth_http_mac` - [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
27
29
  * `oauth_assertion_base` - [Assertion Framework](https://datatracker.ietf.org/doc/html/rfc7521);
28
30
  * `oauth_saml_bearer_grant` - [SAML 2.0 Bearer Assertion](https://datatracker.ietf.org/doc/html/rfc7522);
29
31
  * `oauth_jwt_bearer_grant` - [JWT Bearer Assertion](https://datatracker.ietf.org/doc/html/rfc7523);
30
- * [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
31
- * [Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591);
32
+
33
+ * `oauth_dynamic_client_registration` - [Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591);
32
34
  * OAuth application and token management dashboards;
33
35
 
34
36
  It also implements the [OpenID Connect layer](https://openid.net/connect/) (via the `openid` feature) on top of the OAuth features it provides, including:
@@ -0,0 +1,100 @@
1
+ ## 0.10.0 (10/06/2022)
2
+
3
+ ### Features
4
+
5
+ #### Resource Indicators
6
+
7
+ RFC: https://datatracker.ietf.org/doc/html/rfc8707
8
+
9
+ `rodauth-oauth` now supports Resource Indicators, via the optional `:oauth_resource_indicators` feature.
10
+
11
+ #### JWT: extra options
12
+
13
+ The following extra option values were added:
14
+
15
+ * `oauth_jwt_jwe_keys`
16
+ * `oauth_jwt_public_keys`
17
+ * `oauth_jwt_jwe_public_keys`
18
+
19
+ `:oauth_jwt_jwe_keys` should be used to store all provider combos of encryption keys, indexed by an algo/method tuple:
20
+
21
+ ```ruby
22
+ oauth_jwt_jwe_keys { { %w[RSA-OAEP A128CBC-HS256] => key } }
23
+ ```
24
+
25
+ The first element of the hash should indicate the preferred encryption mode, when no combination is specifically requested.
26
+
27
+ It should be considered the most future-proof way of declaring JWE keys, and support for `oauth_jwt_jwe_key` and friends should be soon deprecated.
28
+
29
+ Both `oauth_jwt_public_keys` and `oauth_jwt_jwe_public_keys` provide a way to declare multiple keys to be exposed as the provider JWKs in the `/jwks` endpoint.
30
+
31
+ ### Improvements
32
+
33
+ * Added translations for portuguese.
34
+
35
+ #### OpenID Connect improvements
36
+
37
+ * The `:oidc` feature now depends on `rodauth`'s [account_expiration](http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html) feature.
38
+
39
+ Although a more-involved-somewhat-breaking change, it was required in order to keep track of account login event timestamps, necessary for correct `"auth_time"` calculation (see the first bugfix mention for more details, and Breaking Changes for migration path).
40
+
41
+
42
+ * Support for the `ui_locales` parameter was added. This feature depends on the `:i18n` feature provided by [rodauth-i18n](https://github.com/janko/rodauth-i18n).
43
+ * Support for the `claims_locales` parameter was added, in that the `get_oidc_param` and `get_additional_param`, when accepting a 3rd parameter, will be passed a locale code:
44
+
45
+ ```ruby
46
+ # given "claims_locales=en pt"
47
+
48
+ get_oidc_param { |account, param, locale| }
49
+ # will be called twice for the same param, one with locale as "en", another as "pt"
50
+
51
+ get_oidc_param { |account, param| }
52
+ # will be called once without locale
53
+ ```
54
+
55
+ * Support for `max_age` parameter was added.
56
+
57
+ * Support for `acr_values` parameter was added.
58
+
59
+ When "phr", and a `rodauth` 2-factor feature (like [otp](http://rodauth.jeremyevans.net/rdoc/files/doc/otp_rdoc.html)) is enabled, the user will be requested for 2-factor authentication before performing the OpenID Authorization Request.
60
+
61
+ When "phrh", and `rodauth`'s [webauthn_login](http://rodauth.jeremyevans.net/rdoc/files/doc/webauthn_login_rdoc.html) feature is enabled, the user will be requested for WebAuthn authentication before performing the OpenID Authorization Request.
62
+
63
+ Any other acr values are considered provider-specific, and the `require_acr_value(acr_value)` option should be provided to deal with it (it'll be called after authentication is ensured and before the authorization request is processed).
64
+
65
+ ### Bugfixes
66
+
67
+ * reverted the `"auth_time"` calculation "fix" introduced in 0.9.3, which broke compliance with the RFC (the implementation prior to that was also broken, hence why `"account_expiration"` plugin was introduced as a dependency).
68
+
69
+ ### Breaking Changes
70
+
71
+ As you read already, the `"account_expiration"` feature is now required by default by `"oidc"`. In order to migrate to it, here's a suggested strategy:
72
+
73
+ 1. Add the relevant database tables
74
+
75
+ Add a migration looking roughly like this:
76
+
77
+ ```ruby
78
+ create_table(:account_activity_times) do
79
+ foreign_key :id, :accounts, primary_key: true, type: Integer
80
+ DateTime :last_activity_at, null: false
81
+ DateTime :last_login_at, null: false
82
+ DateTime :expired_at
83
+ end
84
+ ```
85
+
86
+ 2. Update and deploy `rodauth-oauth` 0.10.0
87
+
88
+ (Nothing required beyond `enable :oidc`.)
89
+
90
+ 3. Set `:last_login_at` to a value.
91
+
92
+ Like now. You can , for example, run this SQL:
93
+
94
+ ```sql
95
+ UPDATE account_activity_times SET last_login_at = CURRENT_TIMESTAMP;
96
+ ```
97
+
98
+ ---
99
+
100
+ That's it, nothing fancy or accurate. Yes, the `last_login_at` is wrong, but as sessions expire, it should go back to normal.
@@ -34,13 +34,37 @@
34
34
  </div>
35
35
  <% end %>
36
36
  <%= hidden_field_tag :client_id, params[:client_id] %>
37
- <% %i[access_type response_type state nonce redirect_uri code_challenge code_challenge_method].each do |oauth_param| %>
37
+ <% %i[access_type response_type response_mode state redirect_uri].each do |oauth_param| %>
38
38
  <% if params[oauth_param] %>
39
39
  <%= hidden_field_tag oauth_param, params[oauth_param] %>
40
40
  <% end %>
41
41
  <% end %>
42
- <% if params[:response_mode] %>
43
- <%= hidden_field_tag :response_mode, params[:response_mode] %>
42
+ <% if rodauth.features.include?(:oauth_resource_indicators) && rodauth.resource_indicators %>
43
+ <% rodauth.resource_indicators.each do |resource| %>
44
+ <%= hidden_field_tag "resource", resource %>
45
+ <% end %>
46
+ <% end %>
47
+ <% if rodauth.features.include?(:oauth_pkce) %>
48
+ <% if params[:code_challenge] %>
49
+ <%= hidden_field_tag :code_challenge, params[:code_challenge] %>
50
+ <% end %>
51
+ <% if params[:code_challenge_method] %>
52
+ <%= hidden_field_tag :code_challenge_method, params[:code_challenge_method] %>
53
+ <% end %>
54
+ <% end %>
55
+ <% if rodauth.features.include?(:oidc) %>
56
+ <% if params[:nonce] %>
57
+ <%= hidden_field_tag :nonce, params[:nonce] %>
58
+ <% end %>
59
+ <% if params[:ui_locales] %>
60
+ <%= hidden_field_tag :ui_locales, params[:ui_locales] %>
61
+ <% end %>
62
+ <% if params[:claims_locales] %>
63
+ <%= hidden_field_tag :claims_locales, params[:claims_locales] %>
64
+ <% end %>
65
+ <% if params[:acr_values] %>
66
+ <%= hidden_field_tag :acr, params[:acr_values] %>
67
+ <% end %>
44
68
  <% end %>
45
69
  </div>
46
70
  <p class="text-center">
@@ -52,6 +52,8 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
52
52
  # device code grant
53
53
  # t.string :user_code, null: true, unique: true
54
54
  # t.datetime :last_polled_at, null: true
55
+ # when using :oauth_resource_indicators feature
56
+ # t.string :resource
55
57
  end
56
58
 
57
59
  create_table :oauth_tokens do |t|
@@ -78,6 +80,8 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
78
80
  # uncomment to use OIDC nonce
79
81
  # t.string :nonce
80
82
  # t.datetime :auth_time
83
+ # when using :oauth_resource_indicators feature
84
+ # t.string :resource
81
85
  end
82
86
  end
83
87
  end
@@ -425,6 +425,11 @@ module Rodauth
425
425
  oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
426
426
  }.merge(params)
427
427
 
428
+ if create_params[oauth_tokens_scopes_column].is_a?(Array)
429
+ create_params[oauth_tokens_scopes_column] =
430
+ create_params[oauth_tokens_scopes_column].join(" ")
431
+ end
432
+
428
433
  rescue_from_uniqueness_error do
429
434
  access_token = _generate_access_token(create_params)
430
435
  refresh_token = _generate_refresh_token(create_params) if should_generate_refresh_token
@@ -44,10 +44,13 @@ module Rodauth
44
44
 
45
45
  auth_value_method :oauth_jwt_keys, {}
46
46
  auth_value_method :oauth_jwt_key, nil
47
+ auth_value_method :oauth_jwt_public_keys, {}
47
48
  auth_value_method :oauth_jwt_public_key, nil
48
49
  auth_value_method :oauth_jwt_algorithm, "RS256"
49
50
 
51
+ auth_value_method :oauth_jwt_jwe_keys, {}
50
52
  auth_value_method :oauth_jwt_jwe_key, nil
53
+ auth_value_method :oauth_jwt_jwe_public_keys, {}
51
54
  auth_value_method :oauth_jwt_jwe_public_key, nil
52
55
  auth_value_method :oauth_jwt_jwe_algorithm, nil
53
56
  auth_value_method :oauth_jwt_jwe_encryption_method, nil
@@ -407,10 +410,11 @@ module Rodauth
407
410
 
408
411
  def jwt_encode(payload,
409
412
  jwks: nil,
410
- jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
411
- signing_algorithm: oauth_jwt_algorithm,
412
413
  encryption_algorithm: oauth_jwt_jwe_algorithm,
413
- encryption_method: oauth_jwt_jwe_encryption_method)
414
+ encryption_method: oauth_jwt_jwe_encryption_method,
415
+ jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm,
416
+ encryption_method]] || oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
417
+ signing_algorithm: oauth_jwt_algorithm || oauth_jwt_keys.keys.first)
414
418
  payload[:jti] = generate_jti(payload)
415
419
  jwt = JSON::JWT.new(payload)
416
420
 
@@ -427,6 +431,7 @@ module Rodauth
427
431
  jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
428
432
  jwe.to_s
429
433
  elsif jwe_key
434
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
430
435
  algorithm = encryption_algorithm.to_sym if encryption_algorithm
431
436
  meth = encryption_method.to_sym if encryption_method
432
437
  jwt.encrypt(jwe_key, algorithm, meth)
@@ -438,18 +443,23 @@ module Rodauth
438
443
  def jwt_decode(
439
444
  token,
440
445
  jwks: nil,
441
- jws_key: oauth_jwt_public_key || _jwt_key,
442
- jws_algorithm: oauth_jwt_algorithm,
443
- jwe_key: oauth_jwt_jwe_key,
446
+ jws_algorithm: oauth_jwt_algorithm || oauth_jwt_public_key.keys.first || oauth_jwt_keys.keys.first,
447
+ jws_key: oauth_jwt_public_key || oauth_jwt_keys[jws_algorithm] || _jwt_key,
444
448
  jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
445
449
  jws_encryption_method: oauth_jwt_jwe_encryption_method,
450
+ jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_key,
446
451
  verify_claims: true,
447
452
  verify_jti: true,
448
453
  verify_iss: true,
449
454
  verify_aud: false,
450
455
  **
451
456
  )
452
- token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if jwe_key
457
+ jws_key = jws_key.first if jws_key.is_a?(Array)
458
+
459
+ if jwe_key
460
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
461
+ token = JSON::JWT.decode(token, jwe_key).plain_text
462
+ end
453
463
 
454
464
  claims = if is_authorization_server?
455
465
  if oauth_jwt_legacy_public_key
@@ -487,6 +497,21 @@ module Rodauth
487
497
 
488
498
  def jwks_set
489
499
  @jwks_set ||= [
500
+ *(
501
+ unless oauth_jwt_public_keys.empty?
502
+ oauth_jwt_public_keys.flat_map { |algo, pkeys| pkeys.map { |pkey| JSON::JWK.new(pkey).merge(use: "sig", alg: algo) } }
503
+ end
504
+ ),
505
+ *(
506
+ unless oauth_jwt_jwe_public_keys.empty?
507
+ oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
508
+ pkeys.map do |pkey|
509
+ JSON::JWK.new(pkey).merge(use: "enc", alg: algo)
510
+ end
511
+ end
512
+ end
513
+ ),
514
+ # legacy
490
515
  (JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
491
516
  (JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key),
492
517
  (JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
@@ -522,7 +547,8 @@ module Rodauth
522
547
  JWT::JWK.import(data).keypair
523
548
  end
524
549
 
525
- def jwt_encode(payload, signing_algorithm: oauth_jwt_algorithm)
550
+ def jwt_encode(payload,
551
+ signing_algorithm: oauth_jwt_algorithm || oauth_jwt_keys.keys.first)
526
552
  headers = {}
527
553
 
528
554
  key = oauth_jwt_keys[signing_algorithm] || _jwt_key
@@ -545,11 +571,11 @@ module Rodauth
545
571
  def jwt_encode_with_jwe(
546
572
  payload,
547
573
  jwks: nil,
548
- jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
549
574
  encryption_algorithm: oauth_jwt_jwe_algorithm,
550
- encryption_method: oauth_jwt_jwe_encryption_method, **args
575
+ encryption_method: oauth_jwt_jwe_encryption_method,
576
+ jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_keys[[encryption_algorithm, encryption_method]] || oauth_jwt_jwe_key,
577
+ **args
551
578
  )
552
-
553
579
  token = jwt_encode_without_jwe(payload, **args)
554
580
 
555
581
  return token unless encryption_algorithm && encryption_method
@@ -557,6 +583,7 @@ module Rodauth
557
583
  if jwks && jwks.any? { |k| k[:use] == "enc" }
558
584
  JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
559
585
  elsif jwe_key
586
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
560
587
  params = {
561
588
  zip: "DEF",
562
589
  copyright: oauth_jwt_jwe_copyright
@@ -576,13 +603,15 @@ module Rodauth
576
603
  def jwt_decode(
577
604
  token,
578
605
  jwks: nil,
579
- jws_key: oauth_jwt_public_key || _jwt_key,
580
- jws_algorithm: oauth_jwt_algorithm,
606
+ jws_algorithm: oauth_jwt_algorithm || oauth_jwt_public_key.keys.first || oauth_jwt_keys.keys.first,
607
+ jws_key: oauth_jwt_public_key || oauth_jwt_keys[jws_algorithm] || _jwt_key,
581
608
  verify_claims: true,
582
609
  verify_jti: true,
583
610
  verify_iss: true,
584
611
  verify_aud: false
585
612
  )
613
+ jws_key = jws_key.first if jws_key.is_a?(Array)
614
+
586
615
  # verifying the JWT implies verifying:
587
616
  #
588
617
  # issuer: check that server generated the token
@@ -631,15 +660,16 @@ module Rodauth
631
660
  def jwt_decode_with_jwe(
632
661
  token,
633
662
  jwks: nil,
634
- jwe_key: oauth_jwt_jwe_key,
635
663
  jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
636
664
  jws_encryption_method: oauth_jwt_jwe_encryption_method,
665
+ jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_key,
637
666
  **args
638
667
  )
639
668
 
640
669
  token = if jwks && jwks.any? { |k| k[:use] == "enc" }
641
670
  JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
642
671
  elsif jwe_key
672
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
643
673
  JWE.decrypt(token, jwe_key)
644
674
  else
645
675
  token
@@ -656,6 +686,21 @@ module Rodauth
656
686
 
657
687
  def jwks_set
658
688
  @jwks_set ||= [
689
+ *(
690
+ unless oauth_jwt_public_keys.empty?
691
+ oauth_jwt_public_keys.flat_map { |algo, pkeys| pkeys.map { |pkey| JWT::JWK.new(pkey).export.merge(use: "sig", alg: algo) } }
692
+ end
693
+ ),
694
+ *(
695
+ unless oauth_jwt_jwe_public_keys.empty?
696
+ oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
697
+ pkeys.map do |pkey|
698
+ JWT::JWK.new(pkey).export.merge(use: "enc", alg: algo)
699
+ end
700
+ end
701
+ end
702
+ ),
703
+ # legacy
659
704
  (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
660
705
  (
661
706
  if oauth_jwt_legacy_public_key
@@ -0,0 +1,153 @@
1
+ # frozen-string-literal: true
2
+
3
+ require "rodauth/oauth/version"
4
+ require "rodauth/oauth/ttl_store"
5
+
6
+ module Rodauth
7
+ Feature.define(:oauth_resource_indicators, :OauthResourceIndicators) do
8
+ depends :oauth_base
9
+
10
+ auth_value_method :oauth_grants_resource_column, :resource
11
+ auth_value_method :oauth_tokens_resource_column, :resource
12
+
13
+ def resource_indicators
14
+ return @resource_indicators if defined?(@resource_indicators)
15
+
16
+ resources = param_or_nil("resource")
17
+
18
+ return unless resources
19
+
20
+ if json_request? || param_or_nil("request") # signed request
21
+ resources = Array(resources)
22
+ else
23
+ query = request.form_data? ? request.body.read : request.query_string
24
+ # resource query param does not conform to rack parsing rules
25
+ resources = URI.decode_www_form(query).each_with_object([]) do |(k, v), memo|
26
+ memo << v if k == "resource"
27
+ end
28
+ end
29
+
30
+ @resource_indicators = resources
31
+ end
32
+
33
+ def require_oauth_authorization(*)
34
+ super
35
+
36
+ return unless authorization_token[oauth_tokens_resource_column]
37
+
38
+ token_indicators = authorization_token[oauth_tokens_resource_column]
39
+
40
+ token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
41
+
42
+ authorization_required unless token_indicators.any? { |resource| base_url.start_with?(resource) }
43
+ end
44
+
45
+ private
46
+
47
+ def validate_oauth_token_params
48
+ super
49
+
50
+ return unless resource_indicators
51
+
52
+ resource_indicators.each do |resource|
53
+ redirect_response_error("invalid_target") unless check_valid_no_fragment_uri?(resource)
54
+ end
55
+ end
56
+
57
+ def create_oauth_token_from_token(oauth_token, update_params)
58
+ return super unless resource_indicators
59
+
60
+ return super unless oauth_token[oauth_tokens_oauth_grant_id_column]
61
+
62
+ oauth_grant = db[oauth_grants_table].where(
63
+ oauth_grants_id_column => oauth_token[oauth_tokens_oauth_grant_id_column],
64
+ oauth_grants_revoked_at_column => nil
65
+ ).first
66
+
67
+ grant_indicators = oauth_grant[oauth_grants_resource_column]
68
+
69
+ grant_indicators = grant_indicators.split(" ") if grant_indicators.is_a?(String)
70
+
71
+ redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
72
+
73
+ super(oauth_token, update_params.merge(oauth_tokens_resource_column => resource_indicators))
74
+ end
75
+
76
+ def check_valid_no_fragment_uri?(uri)
77
+ check_valid_uri?(uri) && URI.parse(uri).fragment.nil?
78
+ end
79
+
80
+ module IndicatorAuthorizationCodeGrant
81
+ private
82
+
83
+ def validate_oauth_grant_params
84
+ super
85
+
86
+ return unless resource_indicators
87
+
88
+ resource_indicators.each do |resource|
89
+ redirect_response_error("invalid_target") unless check_valid_no_fragment_uri?(resource)
90
+ end
91
+ end
92
+
93
+ def create_oauth_token_from_authorization_code(oauth_grant, create_params)
94
+ return super unless resource_indicators
95
+
96
+ redirect_response_error("invalid_target") unless oauth_grant[oauth_grants_resource_column]
97
+
98
+ grant_indicators = oauth_grant[oauth_grants_resource_column]
99
+
100
+ grant_indicators = grant_indicators.split(" ") if grant_indicators.is_a?(String)
101
+
102
+ redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
103
+
104
+ super(oauth_grant, create_params.merge(oauth_tokens_resource_column => resource_indicators))
105
+ end
106
+
107
+ def create_oauth_grant(create_params = {})
108
+ create_params[oauth_grants_resource_column] = resource_indicators.join(" ") if resource_indicators
109
+
110
+ super
111
+ end
112
+ end
113
+
114
+ module IndicatorIntrospection
115
+ def json_token_introspect_payload(token)
116
+ return super unless token[oauth_tokens_oauth_grant_id_column]
117
+
118
+ payload = super
119
+
120
+ token_indicators = token[oauth_tokens_resource_column]
121
+
122
+ token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
123
+
124
+ payload[:aud] = token_indicators
125
+
126
+ payload
127
+ end
128
+
129
+ def introspection_request(*)
130
+ payload = super
131
+
132
+ payload[oauth_tokens_resource_column] = payload["aud"] if payload["aud"]
133
+
134
+ payload
135
+ end
136
+ end
137
+
138
+ module IndicatorJwt
139
+ def jwt_claims(*)
140
+ return super unless resource_indicators
141
+
142
+ super.merge(aud: resource_indicators)
143
+ end
144
+ end
145
+
146
+ def self.included(rodauth)
147
+ super
148
+ rodauth.send(:include, IndicatorAuthorizationCodeGrant) if rodauth.features.include?(:oauth_authorization_code_grant)
149
+ rodauth.send(:include, IndicatorIntrospection) if rodauth.features.include?(:oauth_token_introspection)
150
+ rodauth.send(:include, IndicatorJwt) if rodauth.features.include?(:oauth_jwt)
151
+ end
152
+ end
153
+ end
@@ -60,7 +60,7 @@ module Rodauth
60
60
  id_token_signing_alg_values_supported
61
61
  ].freeze
62
62
 
63
- depends :oauth_jwt
63
+ depends :account_expiration, :oauth_jwt
64
64
 
65
65
  auth_value_method :oauth_application_default_scope, "openid"
66
66
  auth_value_method :oauth_application_scopes, %w[openid]
@@ -73,8 +73,9 @@ module Rodauth
73
73
  auth_value_method :oauth_applications_userinfo_encrypted_response_enc_column, :userinfo_encrypted_response_enc
74
74
 
75
75
  auth_value_method :oauth_grants_nonce_column, :nonce
76
+ auth_value_method :oauth_grants_acr_column, :acr
76
77
  auth_value_method :oauth_tokens_nonce_column, :nonce
77
- auth_value_method :oauth_tokens_auth_time_column, :auth_time
78
+ auth_value_method :oauth_tokens_acr_column, :acr
78
79
 
79
80
  translatable_method :invalid_scope_message, "The Access Token expired"
80
81
 
@@ -88,7 +89,13 @@ module Rodauth
88
89
  auth_value_method :oauth_applications_post_logout_redirect_uri_column, :post_logout_redirect_uri
89
90
  auth_value_method :use_rp_initiated_logout?, false
90
91
 
91
- auth_value_methods(:get_oidc_param, :get_additional_param)
92
+ auth_value_methods(
93
+ :get_oidc_param,
94
+ :get_additional_param,
95
+ :require_acr_value_phr,
96
+ :require_acr_value_phrh,
97
+ :require_acr_value
98
+ )
92
99
 
93
100
  # /userinfo
94
101
  route(:userinfo) do |r|
@@ -252,14 +259,43 @@ module Rodauth
252
259
 
253
260
  private
254
261
 
262
+ if defined?(::I18n)
263
+ def before_authorize_route
264
+ if (ui_locales = param_or_nil("ui_locales"))
265
+ ui_locales = ui_locales.split(" ").map(&:to_sym)
266
+ ui_locales &= ::I18n.available_locales
267
+
268
+ ::I18n.locale = ui_locales.first unless ui_locales.empty?
269
+ end
270
+
271
+ super
272
+ end
273
+ end
274
+
275
+ def validate_oauth_grant_params
276
+ return super unless (max_age = param_or_nil("max_age"))
277
+
278
+ max_age = Integer(max_age)
279
+
280
+ redirect_response_error("invalid_request") unless max_age.positive?
281
+
282
+ return unless Time.now - last_account_login_at > max_age
283
+
284
+ # force user to re-login
285
+ clear_session
286
+ set_session_value(login_redirect_session_key, request.fullpath)
287
+ redirect require_login_redirect
288
+ end
289
+
255
290
  def require_authorizable_account
256
- try_prompt if param_or_nil("prompt")
291
+ try_prompt
257
292
  super
293
+ try_acr_values
258
294
  end
259
295
 
260
296
  # this executes before checking for a logged in account
261
297
  def try_prompt
262
- prompt = param_or_nil("prompt")
298
+ return unless (prompt = param_or_nil("prompt"))
263
299
 
264
300
  case prompt
265
301
  when "none"
@@ -314,16 +350,46 @@ module Rodauth
314
350
  end
315
351
  end
316
352
 
317
- def create_oauth_grant(create_params = {})
318
- return super unless (nonce = param_or_nil("nonce"))
353
+ def try_acr_values
354
+ return unless (acr_values = param_or_nil("acr_values"))
355
+
356
+ acr_values.split(" ").each do |acr_value|
357
+ case acr_value
358
+ when "phr" then require_acr_value_phr
359
+ when "phrh" then require_acr_value_phrh
360
+ else
361
+ require_acr_value(acr_value)
362
+ end
363
+ end
364
+ end
319
365
 
320
- super(create_params.merge(oauth_grants_nonce_column => nonce))
366
+ def require_acr_value_phr
367
+ return unless respond_to?(:require_two_factor_authenticated)
368
+
369
+ require_two_factor_authenticated
370
+ end
371
+
372
+ def require_acr_value_phrh
373
+ require_acr_value_phr && two_factor_login_type_match?("webauthn")
374
+ end
375
+
376
+ def require_acr_value(_acr); end
377
+
378
+ def create_oauth_grant(create_params = {})
379
+ if (nonce = param_or_nil("nonce"))
380
+ create_params[oauth_grants_nonce_column] = nonce
381
+ end
382
+ if (acr = param_or_nil("acr"))
383
+ create_params[oauth_grants_acr_column] = acr
384
+ end
385
+ super
321
386
  end
322
387
 
323
388
  def create_oauth_token_from_authorization_code(oauth_grant, create_params)
324
- return super unless oauth_grant[oauth_grants_nonce_column]
389
+ create_params[oauth_tokens_nonce_column] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
390
+ create_params[oauth_tokens_acr_column] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
325
391
 
326
- super(oauth_grant, create_params.merge(oauth_tokens_nonce_column => oauth_grant[oauth_grants_nonce_column]))
392
+ super
327
393
  end
328
394
 
329
395
  def create_oauth_token(*)
@@ -338,11 +404,13 @@ module Rodauth
338
404
  return unless oauth_scopes.include?("openid")
339
405
 
340
406
  id_token_claims = jwt_claims(oauth_token)
407
+
341
408
  id_token_claims[:nonce] = oauth_token[oauth_tokens_nonce_column] if oauth_token[oauth_tokens_nonce_column]
342
409
 
410
+ id_token_claims[:acr] = oauth_token[oauth_tokens_acr_column] if oauth_token[oauth_tokens_acr_column]
411
+
343
412
  # Time when the End-User authentication occurred.
344
- #
345
- id_token_claims[:auth_time] = oauth_token[oauth_tokens_auth_time_column].to_i
413
+ id_token_claims[:auth_time] = last_account_login_at.to_i
346
414
 
347
415
  account = db[accounts_table].where(account_id_column => oauth_token[oauth_tokens_account_id_column]).first
348
416
 
@@ -377,16 +445,23 @@ module Rodauth
377
445
 
378
446
  oidc_scopes, additional_scopes = scopes_by_claim.keys.partition { |key| OIDC_SCOPES_MAP.key?(key) }
379
447
 
448
+ if (claims_locales = param_or_nil("claims_locales"))
449
+ claims_locales = claims_locales.split(" ").map(&:to_sym)
450
+ end
451
+
380
452
  unless oidc_scopes.empty?
381
453
  if respond_to?(:get_oidc_param)
454
+ get_oidc_param = proxy_get_param(:get_oidc_param, claims, claims_locales)
455
+
382
456
  oidc_scopes.each do |scope|
383
457
  scope_claims = claims
384
458
  params = scopes_by_claim[scope]
385
459
  params = params.empty? ? OIDC_SCOPES_MAP[scope] : (OIDC_SCOPES_MAP[scope] & params)
386
460
 
387
461
  scope_claims = (claims["address"] = {}) if scope == "address"
462
+
388
463
  params.each do |param|
389
- scope_claims[param] = __send__(:get_oidc_param, account, param)
464
+ get_oidc_param[account, param, scope_claims]
390
465
  end
391
466
  end
392
467
  else
@@ -397,14 +472,39 @@ module Rodauth
397
472
  return if additional_scopes.empty?
398
473
 
399
474
  if respond_to?(:get_additional_param)
475
+ get_additional_param = proxy_get_param(:get_additional_param, claims, claims_locales)
476
+
400
477
  additional_scopes.each do |scope|
401
- claims[scope] = __send__(:get_additional_param, account, scope.to_sym)
478
+ get_additional_param[account, scope.to_sym]
402
479
  end
403
480
  else
404
481
  warn "`get_additional_param(account, claim)` must be implemented to use oidc scopes."
405
482
  end
406
483
  end
407
484
 
485
+ def proxy_get_param(get_param_func, claims, claims_locales)
486
+ meth = method(get_param_func)
487
+ if meth.arity == 2
488
+ ->(account, param, cl = claims) { cl[param] = meth[account, param] }
489
+ elsif claims_locales.nil?
490
+ ->(account, param, cl = claims) { cl[param] = meth[account, param, nil] }
491
+ else
492
+ lambda do |account, param, cl = claims|
493
+ claims_values = claims_locales.map do |locale|
494
+ meth[account, param, locale]
495
+ end
496
+
497
+ if claims_values.uniq.size == 1
498
+ cl[param] = claims_values.first
499
+ else
500
+ claims_locales.zip(claims_values).each do |locale, value|
501
+ cl["#{param}##{locale}"] = value
502
+ end
503
+ end
504
+ end
505
+ end
506
+ end
507
+
408
508
  def json_access_token_payload(oauth_token)
409
509
  payload = super
410
510
  payload["id_token"] = oauth_token[:id_token] if oauth_token[:id_token]
@@ -452,6 +552,12 @@ module Rodauth
452
552
  oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
453
553
  oauth_tokens_scopes_column => scopes
454
554
  }
555
+ if (nonce = param_or_nil("nonce"))
556
+ create_params[oauth_grants_nonce_column] = nonce
557
+ end
558
+ if (acr = param_or_nil("acr"))
559
+ create_params[oauth_grants_acr_column] = acr
560
+ end
455
561
  oauth_token = generate_oauth_token(create_params, false)
456
562
  generate_id_token(oauth_token)
457
563
  params = json_access_token_payload(oauth_token)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.9.3"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
data/locales/en.yml CHANGED
@@ -7,7 +7,7 @@ en:
7
7
  revoke_oauth_token_notice_flash: "The oauth token has been revoked"
8
8
  device_verification_notice_flash: "The device is verified"
9
9
  user_code_not_found_error_flash: "No device to authorize with the given user code"
10
- oauth_authorize_title: "Authorize"
10
+ authorize_page_title: "Authorize"
11
11
  oauth_applications_page_title: "Oauth Applications"
12
12
  oauth_application_page_title: "Oauth Application"
13
13
  new_oauth_application_page_title: "New Oauth Application"
@@ -17,6 +17,7 @@ en:
17
17
  device_search_page_title: "Device Search"
18
18
  oauth_management_pagination_previous_button: "Previous"
19
19
  oauth_management_pagination_next_button: "Next"
20
+ oauth_tokens_scopes_label: "Scopes"
20
21
  oauth_applications_name_label: "Name"
21
22
  oauth_applications_description_label: "Description"
22
23
  oauth_applications_scopes_label: "Default scopes"
data/locales/pt.yml ADDED
@@ -0,0 +1,57 @@
1
+ pt:
2
+ rodauth:
3
+ require_authorization_error_flash: "Autorize para continuar"
4
+ create_oauth_application_error_flash: "Aconteceu um erro ao registar o aplicativo oauth"
5
+ create_oauth_application_notice_flash: "O seu aplicativo oauth foi registado com sucesso"
6
+ revoke_unauthorized_account_error_flash: "Não está autorizado a revogar este token"
7
+ revoke_oauth_token_notice_flash: "O token oauth foi revogado com sucesso"
8
+ device_verification_notice_flash: "O dispositivo foi verificado com sucesso"
9
+ user_code_not_found_error_flash: "Não existe nenhum dispositivo a ser autorizado com o código de usuário inserido"
10
+ authorize_page_title: "Autorizar"
11
+ oauth_applications_page_title: "Aplicativos OAuth"
12
+ oauth_application_page_title: "Aplicativo Oauth"
13
+ new_oauth_application_page_title: "Novo Aplicativo Oauth"
14
+ oauth_application_oauth_tokens_page_title: "Tokens Oauth do Aplicativo"
15
+ oauth_tokens_page_title: "Os meus Tokens Oauth"
16
+ device_verification_page_title: "Verificação de dispositivo"
17
+ device_search_page_title: "Pesquisa de dispositivo"
18
+ oauth_management_pagination_previous_button: "Anterior"
19
+ oauth_management_pagination_next_button: "Próxima"
20
+ oauth_tokens_scopes_label: "Escopos"
21
+ oauth_applications_name_label: "Nome"
22
+ oauth_applications_description_label: "Descrição"
23
+ oauth_applications_scopes_label: "Escopos prédefinidos"
24
+ oauth_applications_contacts_label: "Contactos"
25
+ oauth_applications_homepage_url_label: "URL da página principal"
26
+ oauth_applications_tos_uri_label: "URL dos termos de serviço"
27
+ oauth_applications_policy_uri_label: "URL das diretrizes"
28
+ oauth_applications_redirect_uri_label: "URL para redireccionamento"
29
+ oauth_applications_client_secret_label: "Segredo de cliente"
30
+ oauth_applications_client_id_label: "ID do cliente"
31
+ oauth_grant_user_code_label: "Código do usuário"
32
+ oauth_grant_user_jws_jwk_label: "Chaves JSON Web"
33
+ oauth_grant_user_jwt_public_key_label: "Chave pública"
34
+ oauth_application_button: "Registar"
35
+ oauth_authorize_button: "Autorizar"
36
+ oauth_token_revoke_button: "Revogar"
37
+ oauth_authorize_post_button: "Voltar para o aplicativo cliente"
38
+ oauth_device_verification_button: "Verificar"
39
+ oauth_device_search_button: "Pesquisar"
40
+ invalid_client_message: "A autenticação do cliente falhou"
41
+ invalid_grant_type_message: "Tipo de atribuição inválida"
42
+ invalid_grant_message: "Atribuição inválida"
43
+ invalid_scope_message: "Escopo inválido"
44
+ invalid_url_message: "URL inválido"
45
+ unsupported_token_type_message: "Sugestão de tipo de token inválida"
46
+ unique_error_message: "já está sendo utilizado"
47
+ null_error_message: "não está preenchido"
48
+ already_in_use_message: "erro ao gerar token único"
49
+ expired_token_message: "o código de dispositivo expirou"
50
+ access_denied_message: "o pedido de autorização foi negado"
51
+ authorization_pending_message: "o pedido de autorização ainda está pendente"
52
+ slow_down_message: "o pedido de autorização ainda está pendente mas o intervalo de actualização deve ser aumentado"
53
+ code_challenge_required_message: "código de negociação necessário"
54
+ unsupported_transform_algorithm_message: "algoritmo de transformação não suportado"
55
+ request_uri_not_supported_message: "request_uri não é suportado"
56
+ invalid_request_object_message: "request_object é inválido"
57
+ invalid_scope_message: "O Token de acesso expirou"
@@ -81,10 +81,20 @@
81
81
  #{"<input type=\"hidden\" name=\"response_type\" value=\"#{rodauth.param("response_type")}\"/>" if rodauth.param_or_nil("response_type")}
82
82
  #{"<input type=\"hidden\" name=\"response_mode\" value=\"#{rodauth.param("response_mode")}\"/>" if rodauth.param_or_nil("response_mode")}
83
83
  #{"<input type=\"hidden\" name=\"state\" value=\"#{rodauth.param("state")}\"/>" if rodauth.param_or_nil("state")}
84
- #{"<input type=\"hidden\" name=\"nonce\" value=\"#{rodauth.param("nonce")}\"/>" if rodauth.param_or_nil("nonce")}
85
84
  #{"<input type=\"hidden\" name=\"redirect_uri\" value=\"#{rodauth.redirect_uri}\"/>" if rodauth.param_or_nil("redirect_uri")}
86
- #{"<input type=\"hidden\" name=\"code_challenge\" value=\"#{rodauth.param("code_challenge")}\"/>" if rodauth.param_or_nil("code_challenge")}
87
- #{"<input type=\"hidden\" name=\"code_challenge_method\" value=\"#{rodauth.param("code_challenge_method")}\"/>" if rodauth.param_or_nil("code_challenge_method")}
85
+ #{"<input type=\"hidden\" name=\"code_challenge\" value=\"#{rodauth.param("code_challenge")}\"/>" if rodauth.features.include?(:oauth_pkce) && rodauth.param_or_nil("code_challenge")}
86
+ #{"<input type=\"hidden\" name=\"code_challenge_method\" value=\"#{rodauth.param("code_challenge_method")}\"/>" if rodauth.features.include?(:oauth_pkce) && rodauth.param_or_nil("code_challenge_method")}
87
+ #{"<input type=\"hidden\" name=\"nonce\" value=\"#{rodauth.param("nonce")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("nonce")}
88
+ #{"<input type=\"hidden\" name=\"ui_locales\" value=\"#{rodauth.param("ui_locales")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("ui_locales")}
89
+ #{"<input type=\"hidden\" name=\"claims_locales\" value=\"#{rodauth.param("claims_locales")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("claims_locales")}
90
+ #{"<input type=\"hidden\" name=\"acr\" value=\"#{rodauth.param("acr_values")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("acr_values")}
91
+ #{
92
+ if rodauth.features.include?(:oauth_resource_indicators) && rodauth.resource_indicators
93
+ rodauth.resource_indicators.map do |resource|
94
+ "<input type=\"hidden\" name=\"resource\" value=\"#{resource}\"/>"
95
+ end.join
96
+ end
97
+ }
88
98
  </div>
89
99
  <p class="text-center">
90
100
  <input type="submit" class="btn btn-outline-primary" value="#{h(rodauth.oauth_authorize_button)}"/>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth-oauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.3
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-30 00:00:00.000000000 Z
11
+ date: 2022-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rodauth
@@ -39,6 +39,7 @@ extra_rdoc_files:
39
39
  - doc/release_notes/0_0_4.md
40
40
  - doc/release_notes/0_0_5.md
41
41
  - doc/release_notes/0_0_6.md
42
+ - doc/release_notes/0_10_0.md
42
43
  - doc/release_notes/0_1_0.md
43
44
  - doc/release_notes/0_2_0.md
44
45
  - doc/release_notes/0_3_0.md
@@ -70,6 +71,7 @@ files:
70
71
  - doc/release_notes/0_0_4.md
71
72
  - doc/release_notes/0_0_5.md
72
73
  - doc/release_notes/0_0_6.md
74
+ - doc/release_notes/0_10_0.md
73
75
  - doc/release_notes/0_1_0.md
74
76
  - doc/release_notes/0_2_0.md
75
77
  - doc/release_notes/0_3_0.md
@@ -120,6 +122,7 @@ files:
120
122
  - lib/rodauth/features/oauth_jwt_bearer_grant.rb
121
123
  - lib/rodauth/features/oauth_management_base.rb
122
124
  - lib/rodauth/features/oauth_pkce.rb
125
+ - lib/rodauth/features/oauth_resource_indicators.rb
123
126
  - lib/rodauth/features/oauth_resource_server.rb
124
127
  - lib/rodauth/features/oauth_saml_bearer_grant.rb
125
128
  - lib/rodauth/features/oauth_token_introspection.rb
@@ -135,6 +138,7 @@ files:
135
138
  - lib/rodauth/oauth/ttl_store.rb
136
139
  - lib/rodauth/oauth/version.rb
137
140
  - locales/en.yml
141
+ - locales/pt.yml
138
142
  - templates/authorize.str
139
143
  - templates/client_secret_field.str
140
144
  - templates/description_field.str