rodauth-oauth 0.9.3 → 0.10.0

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