rodauth-oauth 0.9.1 → 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: 8d0412f0fc70f27a32d2517afbc688eae79304a52fd074298ecac3176edf2ee8
4
- data.tar.gz: 2bed00e6896786192f3a4b93b145e8b75b0813741b323213586d736745323617
3
+ metadata.gz: f9b68ff6e15b91128db72a07fa91b86afb70352f9582fa8c27e7abfe3c0dc17c
4
+ data.tar.gz: 1c35b67bc10619c8de31cbcef514636e7975307a0cfc02585ae10ec97de74be1
5
5
  SHA512:
6
- metadata.gz: d04277337c21a48a9b0504eaadac11342bd69a0892e1ee7bd7114880b35fe1cdf4e086044d8fa6198c82da3b8f49b6e12be58b98316f592ed980733d2c2cdaa7
7
- data.tar.gz: f3b8ebe3574ff7559c827a42b76ad698b3a33ba93593fe09695a833af92709ca39b33a9e3be0d1c57c6300666de97850810f37e629a29e59bddd0b8746f63f10
6
+ metadata.gz: 2cf0e357529093b45834697c54bae5eaf17419885e04ccba279d18e65464aa8d8fb2e49da09dd5c96c83331e0f60915e993af5dfc7decfff4c8752b5401dfe8a
7
+ data.tar.gz: 784d5184526ff8dcbc3c112eb58311705baf82e1bf17b40b87cd55dc43f0b06cc6bf7c2cb71f67caf315008b14d3fe4b9fe8eea991a0475586bf4effb0d77ed3
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Rodauth::Oauth
2
2
 
3
- [![pipeline status](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/pipelines?page=1&ref=master)
3
+ [![Gem Version](https://badge.fury.io/rb/rodauth-oauth.svg)](http://rubygems.org/gems/rodauth-oauth)
4
+ [![pipeline status](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/pipelines?page=1&scope=all&ref=master)
4
5
  [![coverage report](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/coverage.svg?job=coverage)](https://honeyryderchuck.gitlab.io/rodauth-oauth/coverage/#_AllFiles)
5
6
 
6
7
  This is an extension to the `rodauth` gem which implements the [OAuth 2.0 framework](https://tools.ietf.org/html/rfc6749) for an authorization server.
@@ -20,14 +21,16 @@ This gem implements the following RFCs and features of OAuth:
20
21
  * `oauth_token_introspection` - [Token introspection](https://tools.ietf.org/html/rfc7662);
21
22
  * [Authorization Server Metadata](https://tools.ietf.org/html/rfc8414);
22
23
  * `oauth_pkce` - [PKCE](https://tools.ietf.org/html/rfc7636);
23
- * Access Type (Token refresh online and offline);
24
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);
25
28
  * `oauth_http_mac` - [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
26
29
  * `oauth_assertion_base` - [Assertion Framework](https://datatracker.ietf.org/doc/html/rfc7521);
27
30
  * `oauth_saml_bearer_grant` - [SAML 2.0 Bearer Assertion](https://datatracker.ietf.org/doc/html/rfc7522);
28
31
  * `oauth_jwt_bearer_grant` - [JWT Bearer Assertion](https://datatracker.ietf.org/doc/html/rfc7523);
29
- * [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
30
- * [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);
31
34
  * OAuth application and token management dashboards;
32
35
 
33
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:
@@ -73,7 +76,7 @@ Or install it yourself as:
73
76
 
74
77
  ## Usage
75
78
 
76
- This tutorial assumes you already read the documentation and know how to set up `rodauth`. After that, integrating `roda-auth` will look like:
79
+ This tutorial assumes you already read the documentation and know how to set up `rodauth`. After that, integrating `rodauth-oauth` will look like:
77
80
 
78
81
  ```ruby
79
82
  plugin :rodauth do
@@ -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.
@@ -0,0 +1,10 @@
1
+ ### 0.9.2 (11/05/2022)
2
+
3
+ #### Bugfixes
4
+
5
+ * Fixed remaining namespacing fix issues requiring usage of `require "rodauth-oauth"`.
6
+ * Fixed wrong expectation of database for resource-server mode when `:oauth_management_base` plugin was used.
7
+ * oidc: fixed incorrect grant creation flow whenn using `nonce` param.
8
+ * oidc: fixed jwt encoding regression when not setting encryption method/algorithmm for client applications.
9
+ * templates: added missing jwks field to the "New oauth application" form.
10
+ * Several fixes on the example OIDC applications, mostly around CSRF breakage when using latest version of `omniauth`.
@@ -0,0 +1,9 @@
1
+ ### 0.9.2 (30/05/2022)
2
+
3
+ #### Bugfixes
4
+
5
+ * `oauth_jwt`: new access tokens generated via the `"refresh_token"` grant type are now JWT (it was falling back to non JWT behaviour);
6
+ * `oidc`: a new `id_token` is now generated via the `"refresh_token"` grant type with "rotation" policy (it was being omitted from the response);
7
+ * `oidc`: fixing calculation of `"auth_time"` claim, which (as per RFC) needs to stay the same across first authentication and subsequent `"refresh_token"` requests;
8
+ * it requires a new db column (default: `"auth_time"`, datetime) in the `"oauth_tokens"` database;
9
+ * hash-column `"refresh_token"` will now expose the refresh token (instead of the hash column version) in the `"refresh_token"` grant type response payload (only happened in "non-rotation" refresh token mode).
@@ -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|
@@ -77,6 +79,9 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
77
79
  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
78
80
  # uncomment to use OIDC nonce
79
81
  # t.string :nonce
82
+ # t.datetime :auth_time
83
+ # when using :oauth_resource_indicators feature
84
+ # t.string :resource
80
85
  end
81
86
  end
82
87
  end
@@ -33,7 +33,7 @@ module Rodauth
33
33
 
34
34
  translatable_method :oauth_applications_name_label, "Name"
35
35
  translatable_method :oauth_applications_description_label, "Description"
36
- translatable_method :oauth_applications_scopes_label, "Scopes"
36
+ translatable_method :oauth_applications_scopes_label, "Default scopes"
37
37
  translatable_method :oauth_applications_contacts_label, "Contacts"
38
38
  translatable_method :oauth_applications_tos_uri_label, "Terms of service"
39
39
  translatable_method :oauth_applications_policy_uri_label, "Policy"
@@ -425,33 +425,46 @@ 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
- token = oauth_unique_id_generator
434
+ access_token = _generate_access_token(create_params)
435
+ refresh_token = _generate_refresh_token(create_params) if should_generate_refresh_token
436
+ oauth_token = _store_oauth_token(create_params)
437
+ oauth_token[oauth_tokens_token_column] = access_token
438
+ oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
439
+ oauth_token
440
+ end
441
+ end
430
442
 
431
- if oauth_tokens_token_hash_column
432
- create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
433
- else
434
- create_params[oauth_tokens_token_column] = token
435
- end
443
+ def _generate_access_token(params = {})
444
+ token = oauth_unique_id_generator
436
445
 
437
- refresh_token = nil
438
- if should_generate_refresh_token
439
- refresh_token = oauth_unique_id_generator
446
+ if oauth_tokens_token_hash_column
447
+ params[oauth_tokens_token_hash_column] = generate_token_hash(token)
448
+ else
449
+ params[oauth_tokens_token_column] = token
450
+ end
440
451
 
441
- if oauth_tokens_refresh_token_hash_column
442
- create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
443
- else
444
- create_params[oauth_tokens_refresh_token_column] = refresh_token
445
- end
446
- end
447
- oauth_token = _generate_oauth_token(create_params)
448
- oauth_token[oauth_tokens_token_column] = token
449
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
450
- oauth_token
452
+ token
453
+ end
454
+
455
+ def _generate_refresh_token(params)
456
+ token = oauth_unique_id_generator
457
+
458
+ if oauth_tokens_refresh_token_hash_column
459
+ params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(token)
460
+ else
461
+ params[oauth_tokens_refresh_token_column] = token
451
462
  end
463
+
464
+ token
452
465
  end
453
466
 
454
- def _generate_oauth_token(params = {})
467
+ def _store_oauth_token(params = {})
455
468
  ds = db[oauth_tokens_table]
456
469
 
457
470
  if __one_oauth_token_per_account
@@ -577,43 +590,24 @@ module Rodauth
577
590
 
578
591
  rescue_from_uniqueness_error do
579
592
  oauth_tokens_ds = db[oauth_tokens_table]
580
- token = oauth_unique_id_generator
593
+ access_token = _generate_access_token(update_params)
594
+
595
+ if oauth_refresh_token_protection_policy == "rotation"
596
+ update_params = {
597
+ **update_params,
598
+ oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
599
+ oauth_tokens_account_id_column => oauth_token[oauth_tokens_account_id_column],
600
+ oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
601
+ }
581
602
 
582
- if oauth_tokens_token_hash_column
583
- update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
603
+ refresh_token = _generate_refresh_token(update_params)
584
604
  else
585
- update_params[oauth_tokens_token_column] = token
605
+ refresh_token = param("refresh_token")
586
606
  end
607
+ oauth_token = __update_and_return__(oauth_tokens_ds, update_params)
587
608
 
588
- oauth_token = if oauth_refresh_token_protection_policy == "rotation"
589
- insert_params = {
590
- **update_params,
591
- oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
592
- oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
593
- }
594
-
595
- refresh_token = oauth_unique_id_generator
596
-
597
- if oauth_tokens_refresh_token_hash_column
598
- insert_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
599
- else
600
- insert_params[oauth_tokens_refresh_token_column] = refresh_token
601
- end
602
-
603
- # revoke the refresh token
604
- oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
605
- .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
606
-
607
- insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column]
608
- __insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params)
609
- else
610
- # includes none
611
- ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
612
- __update_and_return__(ds, update_params)
613
- end
614
-
615
- oauth_token[oauth_tokens_token_column] = token
616
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
609
+ oauth_token[oauth_tokens_token_column] = access_token
610
+ oauth_token[oauth_tokens_refresh_token_column] = refresh_token
617
611
  oauth_token
618
612
  end
619
613
  end
@@ -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
@@ -38,12 +39,18 @@ module Rodauth
38
39
 
39
40
  translatable_method :oauth_applications_jwt_public_key_label, "Public key"
40
41
 
42
+ auth_value_method :oauth_application_jwt_public_key_param, "jwt_public_key"
43
+ auth_value_method :oauth_application_jwks_param, "jwks"
44
+
41
45
  auth_value_method :oauth_jwt_keys, {}
42
46
  auth_value_method :oauth_jwt_key, nil
47
+ auth_value_method :oauth_jwt_public_keys, {}
43
48
  auth_value_method :oauth_jwt_public_key, nil
44
49
  auth_value_method :oauth_jwt_algorithm, "RS256"
45
50
 
51
+ auth_value_method :oauth_jwt_jwe_keys, {}
46
52
  auth_value_method :oauth_jwt_jwe_key, nil
53
+ auth_value_method :oauth_jwt_jwe_public_keys, {}
47
54
  auth_value_method :oauth_jwt_jwe_public_key, nil
48
55
  auth_value_method :oauth_jwt_jwe_algorithm, nil
49
56
  auth_value_method :oauth_jwt_jwe_encryption_method, nil
@@ -62,7 +69,6 @@ module Rodauth
62
69
  :jwt_encode,
63
70
  :jwt_decode,
64
71
  :jwks_set,
65
- :last_account_login_at,
66
72
  :generate_jti
67
73
  )
68
74
 
@@ -95,12 +101,6 @@ module Rodauth
95
101
 
96
102
  private
97
103
 
98
- unless method_defined?(:last_account_login_at)
99
- def last_account_login_at
100
- nil
101
- end
102
- end
103
-
104
104
  def issuer
105
105
  @issuer ||= oauth_jwt_token_issuer || authorization_server_url
106
106
  end
@@ -171,41 +171,38 @@ module Rodauth
171
171
 
172
172
  # /token
173
173
 
174
- def generate_oauth_token(params = {}, should_generate_refresh_token = true)
175
- create_params = {
176
- oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
177
- }.merge(params)
178
-
179
- oauth_token = rescue_from_uniqueness_error do
180
- if should_generate_refresh_token
181
- refresh_token = oauth_unique_id_generator
182
-
183
- if oauth_tokens_refresh_token_hash_column
184
- create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
185
- else
186
- create_params[oauth_tokens_refresh_token_column] = refresh_token
187
- end
188
- end
174
+ def create_oauth_token_from_token(oauth_token, update_params)
175
+ otoken = super
176
+ access_token = _generate_jwt_access_token(otoken)
177
+ otoken[oauth_tokens_token_column] = access_token
178
+ otoken
179
+ end
189
180
 
190
- _generate_oauth_token(create_params)
191
- end
181
+ def generate_oauth_token(params = {}, should_generate_refresh_token = true)
182
+ oauth_token = super
183
+ access_token = _generate_jwt_access_token(oauth_token)
184
+ oauth_token[oauth_tokens_token_column] = access_token
185
+ oauth_token
186
+ end
192
187
 
188
+ def _generate_jwt_access_token(oauth_token)
193
189
  claims = jwt_claims(oauth_token)
194
190
 
195
191
  # one of the points of using jwt is avoiding database lookups, so we put here all relevant
196
192
  # token data.
197
193
  claims[:scope] = oauth_token[oauth_tokens_scopes_column]
198
194
 
199
- token = jwt_encode(claims)
195
+ jwt_encode(claims)
196
+ end
200
197
 
201
- oauth_token[oauth_tokens_token_column] = token
202
- oauth_token
198
+ def _generate_access_token(*)
199
+ # no op
203
200
  end
204
201
 
205
202
  def jwt_claims(oauth_token)
206
203
  issued_at = Time.now.to_i
207
204
 
208
- claims = {
205
+ {
209
206
  iss: issuer, # issuer
210
207
  iat: issued_at, # issued at
211
208
  #
@@ -223,10 +220,6 @@ module Rodauth
223
220
  exp: issued_at + oauth_token_expires_in,
224
221
  aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column])
225
222
  }
226
-
227
- claims[:auth_time] = last_account_login_at.to_i if last_account_login_at
228
-
229
- claims
230
223
  end
231
224
 
232
225
  def jwt_subject(oauth_token)
@@ -417,10 +410,11 @@ module Rodauth
417
410
 
418
411
  def jwt_encode(payload,
419
412
  jwks: nil,
420
- jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
421
- signing_algorithm: oauth_jwt_algorithm,
422
413
  encryption_algorithm: oauth_jwt_jwe_algorithm,
423
- 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)
424
418
  payload[:jti] = generate_jti(payload)
425
419
  jwt = JSON::JWT.new(payload)
426
420
 
@@ -437,6 +431,7 @@ module Rodauth
437
431
  jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
438
432
  jwe.to_s
439
433
  elsif jwe_key
434
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
440
435
  algorithm = encryption_algorithm.to_sym if encryption_algorithm
441
436
  meth = encryption_method.to_sym if encryption_method
442
437
  jwt.encrypt(jwe_key, algorithm, meth)
@@ -448,18 +443,23 @@ module Rodauth
448
443
  def jwt_decode(
449
444
  token,
450
445
  jwks: nil,
451
- jws_key: oauth_jwt_public_key || _jwt_key,
452
- jws_algorithm: oauth_jwt_algorithm,
453
- 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,
454
448
  jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
455
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,
456
451
  verify_claims: true,
457
452
  verify_jti: true,
458
453
  verify_iss: true,
459
454
  verify_aud: false,
460
455
  **
461
456
  )
462
- 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
463
463
 
464
464
  claims = if is_authorization_server?
465
465
  if oauth_jwt_legacy_public_key
@@ -497,6 +497,21 @@ module Rodauth
497
497
 
498
498
  def jwks_set
499
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
500
515
  (JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
501
516
  (JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key),
502
517
  (JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
@@ -532,7 +547,8 @@ module Rodauth
532
547
  JWT::JWK.import(data).keypair
533
548
  end
534
549
 
535
- 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)
536
552
  headers = {}
537
553
 
538
554
  key = oauth_jwt_keys[signing_algorithm] || _jwt_key
@@ -555,11 +571,11 @@ module Rodauth
555
571
  def jwt_encode_with_jwe(
556
572
  payload,
557
573
  jwks: nil,
558
- jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
559
574
  encryption_algorithm: oauth_jwt_jwe_algorithm,
560
- 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
561
578
  )
562
-
563
579
  token = jwt_encode_without_jwe(payload, **args)
564
580
 
565
581
  return token unless encryption_algorithm && encryption_method
@@ -567,6 +583,7 @@ module Rodauth
567
583
  if jwks && jwks.any? { |k| k[:use] == "enc" }
568
584
  JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
569
585
  elsif jwe_key
586
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
570
587
  params = {
571
588
  zip: "DEF",
572
589
  copyright: oauth_jwt_jwe_copyright
@@ -586,13 +603,15 @@ module Rodauth
586
603
  def jwt_decode(
587
604
  token,
588
605
  jwks: nil,
589
- jws_key: oauth_jwt_public_key || _jwt_key,
590
- 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,
591
608
  verify_claims: true,
592
609
  verify_jti: true,
593
610
  verify_iss: true,
594
611
  verify_aud: false
595
612
  )
613
+ jws_key = jws_key.first if jws_key.is_a?(Array)
614
+
596
615
  # verifying the JWT implies verifying:
597
616
  #
598
617
  # issuer: check that server generated the token
@@ -641,15 +660,16 @@ module Rodauth
641
660
  def jwt_decode_with_jwe(
642
661
  token,
643
662
  jwks: nil,
644
- jwe_key: oauth_jwt_jwe_key,
645
663
  jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
646
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,
647
666
  **args
648
667
  )
649
668
 
650
669
  token = if jwks && jwks.any? { |k| k[:use] == "enc" }
651
670
  JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
652
671
  elsif jwe_key
672
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
653
673
  JWE.decrypt(token, jwe_key)
654
674
  else
655
675
  token
@@ -666,6 +686,21 @@ module Rodauth
666
686
 
667
687
  def jwks_set
668
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
669
704
  (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
670
705
  (
671
706
  if oauth_jwt_legacy_public_key
@@ -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
@@ -48,6 +48,10 @@ module Rodauth
48
48
 
49
49
  def post_configure
50
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
+
51
55
  db.extension :pagination
52
56
  end
53
57
 
@@ -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,7 +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
78
+ auth_value_method :oauth_tokens_acr_column, :acr
77
79
 
78
80
  translatable_method :invalid_scope_message, "The Access Token expired"
79
81
 
@@ -87,7 +89,13 @@ module Rodauth
87
89
  auth_value_method :oauth_applications_post_logout_redirect_uri_column, :post_logout_redirect_uri
88
90
  auth_value_method :use_rp_initiated_logout?, false
89
91
 
90
- 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
+ )
91
99
 
92
100
  # /userinfo
93
101
  route(:userinfo) do |r|
@@ -120,7 +128,8 @@ module Rodauth
120
128
  jwks: oauth_application_jwks,
121
129
  encryption_algorithm: @oauth_application[oauth_applications_userinfo_encrypted_response_alg_column],
122
130
  encryption_method: @oauth_application[oauth_applications_userinfo_encrypted_response_enc_column]
123
- }
131
+ }.compact
132
+
124
133
  jwt = jwt_encode(
125
134
  oidc_claims,
126
135
  signing_algorithm: algo,
@@ -250,14 +259,43 @@ module Rodauth
250
259
 
251
260
  private
252
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
+
253
290
  def require_authorizable_account
254
- try_prompt if param_or_nil("prompt")
291
+ try_prompt
255
292
  super
293
+ try_acr_values
256
294
  end
257
295
 
258
296
  # this executes before checking for a logged in account
259
297
  def try_prompt
260
- prompt = param_or_nil("prompt")
298
+ return unless (prompt = param_or_nil("prompt"))
261
299
 
262
300
  case prompt
263
301
  when "none"
@@ -312,16 +350,46 @@ module Rodauth
312
350
  end
313
351
  end
314
352
 
315
- def create_oauth_grant(create_params = {})
316
- 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
365
+
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
317
377
 
318
- super(oauth_grants_nonce_column => nonce)
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
319
386
  end
320
387
 
321
388
  def create_oauth_token_from_authorization_code(oauth_grant, create_params)
322
- 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]
323
391
 
324
- super(oauth_grant, create_params.merge(oauth_tokens_nonce_column => oauth_grant[oauth_grants_nonce_column]))
392
+ super
325
393
  end
326
394
 
327
395
  def create_oauth_token(*)
@@ -336,12 +404,13 @@ module Rodauth
336
404
  return unless oauth_scopes.include?("openid")
337
405
 
338
406
  id_token_claims = jwt_claims(oauth_token)
407
+
339
408
  id_token_claims[:nonce] = oauth_token[oauth_tokens_nonce_column] if oauth_token[oauth_tokens_nonce_column]
340
409
 
410
+ id_token_claims[:acr] = oauth_token[oauth_tokens_acr_column] if oauth_token[oauth_tokens_acr_column]
411
+
341
412
  # Time when the End-User authentication occurred.
342
- #
343
- # Sounds like the same as issued at claim.
344
- id_token_claims[:auth_time] = id_token_claims[:iat]
413
+ id_token_claims[:auth_time] = last_account_login_at.to_i
345
414
 
346
415
  account = db[accounts_table].where(account_id_column => oauth_token[oauth_tokens_account_id_column]).first
347
416
 
@@ -357,7 +426,8 @@ module Rodauth
357
426
  signing_algorithm: oauth_application[oauth_applications_id_token_signed_response_alg_column] || oauth_jwt_algorithm,
358
427
  encryption_algorithm: oauth_application[oauth_applications_id_token_encrypted_response_alg_column],
359
428
  encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
360
- }
429
+ }.compact
430
+
361
431
  oauth_token[:id_token] = jwt_encode(id_token_claims, **params)
362
432
  end
363
433
 
@@ -375,16 +445,23 @@ module Rodauth
375
445
 
376
446
  oidc_scopes, additional_scopes = scopes_by_claim.keys.partition { |key| OIDC_SCOPES_MAP.key?(key) }
377
447
 
448
+ if (claims_locales = param_or_nil("claims_locales"))
449
+ claims_locales = claims_locales.split(" ").map(&:to_sym)
450
+ end
451
+
378
452
  unless oidc_scopes.empty?
379
453
  if respond_to?(:get_oidc_param)
454
+ get_oidc_param = proxy_get_param(:get_oidc_param, claims, claims_locales)
455
+
380
456
  oidc_scopes.each do |scope|
381
457
  scope_claims = claims
382
458
  params = scopes_by_claim[scope]
383
459
  params = params.empty? ? OIDC_SCOPES_MAP[scope] : (OIDC_SCOPES_MAP[scope] & params)
384
460
 
385
461
  scope_claims = (claims["address"] = {}) if scope == "address"
462
+
386
463
  params.each do |param|
387
- scope_claims[param] = __send__(:get_oidc_param, account, param)
464
+ get_oidc_param[account, param, scope_claims]
388
465
  end
389
466
  end
390
467
  else
@@ -395,14 +472,39 @@ module Rodauth
395
472
  return if additional_scopes.empty?
396
473
 
397
474
  if respond_to?(:get_additional_param)
475
+ get_additional_param = proxy_get_param(:get_additional_param, claims, claims_locales)
476
+
398
477
  additional_scopes.each do |scope|
399
- claims[scope] = __send__(:get_additional_param, account, scope.to_sym)
478
+ get_additional_param[account, scope.to_sym]
400
479
  end
401
480
  else
402
481
  warn "`get_additional_param(account, claim)` must be implemented to use oidc scopes."
403
482
  end
404
483
  end
405
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
+
406
508
  def json_access_token_payload(oauth_token)
407
509
  payload = super
408
510
  payload["id_token"] = oauth_token[:id_token] if oauth_token[:id_token]
@@ -450,6 +552,12 @@ module Rodauth
450
552
  oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
451
553
  oauth_tokens_scopes_column => scopes
452
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
453
561
  oauth_token = generate_oauth_token(create_params, false)
454
562
  generate_id_token(oauth_token)
455
563
  params = json_access_token_payload(oauth_token)
@@ -486,7 +594,7 @@ module Rodauth
486
594
  end
487
595
  end
488
596
 
489
- scope_claims.unshift("auth_time") if last_account_login_at
597
+ scope_claims.unshift("auth_time")
490
598
 
491
599
  response_types_supported = metadata[:response_types_supported]
492
600
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.9.1"
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,9 +17,10 @@ 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
- oauth_applications_scopes_label: "Scopes"
23
+ oauth_applications_scopes_label: "Default scopes"
23
24
  oauth_applications_contacts_label: "Contacts"
24
25
  oauth_applications_homepage_url_label: "Homepage URL"
25
26
  oauth_applications_tos_uri_label: "Terms of Service URL"
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)}"/>
@@ -1,4 +1,4 @@
1
1
  <div class="form-group">
2
2
  <label for="name">#{rodauth.oauth_applications_jwks_label}#{rodauth.input_field_label_suffix}</label>
3
- #{rodauth.input_field_string(rodauth.oauth_application_jwks_param, "jwks", :type=>"text")}
3
+ <textarea id="jwks" class="form-control" name="#{rodauth.oauth_application_jwks_param}" rows="3"></textarea>
4
4
  </div>
@@ -1,4 +1,4 @@
1
1
  <div class="form-group">
2
2
  <label for="name">#{rodauth.oauth_applications_jwt_public_key_label}#{rodauth.input_field_label_suffix}</label>
3
- #{rodauth.input_field_string(rodauth.oauth_application_jwt_public_key_param, "jwt_public_key", :type=>"text")}
3
+ #{rodauth.input_field_string(rodauth.oauth_application_jwt_public_key_param, "jwt_public_key", :type=>"text", :required=>false)}
4
4
  </div>
@@ -11,7 +11,7 @@
11
11
  if rodauth.features.include?(:oauth_jwt)
12
12
  <<-HTML
13
13
  #{rodauth.render('jwt_public_key_field')}
14
- #{rodauth.render('jws_jwk_field')}
14
+ #{rodauth.render('jwks_field')}
15
15
  HTML
16
16
  end
17
17
  }
@@ -1,8 +1,9 @@
1
1
  <fieldset class="form-group">
2
+ <legend>#{rodauth.oauth_applications_scopes_label}</legend>
2
3
  #{
3
4
  rodauth.oauth_application_scopes.map do |scope|
4
- "<div class=\"form-check checkbox\">" +
5
- "<input id=\"#{scope}\" type=\"checkbox\" name=\"#{rodauth.oauth_application_scopes_param}[]\" value=\"#{scope}\">" +
5
+ "<div class=\"form-group form-check\">" +
6
+ "<input id=\"#{scope}\" type=\"checkbox\" class=\"form-check-input\" name=\"#{rodauth.oauth_application_scopes_param}[]\" value=\"#{scope}\">" +
6
7
  "<label for=\"#{scope}\">#{scope}</label>" +
7
8
  "</div>"
8
9
  end.join
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.1
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-08 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
@@ -58,6 +59,8 @@ extra_rdoc_files:
58
59
  - doc/release_notes/0_8_0.md
59
60
  - doc/release_notes/0_9_0.md
60
61
  - doc/release_notes/0_9_1.md
62
+ - doc/release_notes/0_9_2.md
63
+ - doc/release_notes/0_9_3.md
61
64
  files:
62
65
  - CHANGELOG.md
63
66
  - LICENSE.txt
@@ -68,6 +71,7 @@ files:
68
71
  - doc/release_notes/0_0_4.md
69
72
  - doc/release_notes/0_0_5.md
70
73
  - doc/release_notes/0_0_6.md
74
+ - doc/release_notes/0_10_0.md
71
75
  - doc/release_notes/0_1_0.md
72
76
  - doc/release_notes/0_2_0.md
73
77
  - doc/release_notes/0_3_0.md
@@ -87,6 +91,8 @@ files:
87
91
  - doc/release_notes/0_8_0.md
88
92
  - doc/release_notes/0_9_0.md
89
93
  - doc/release_notes/0_9_1.md
94
+ - doc/release_notes/0_9_2.md
95
+ - doc/release_notes/0_9_3.md
90
96
  - lib/generators/rodauth/oauth/install_generator.rb
91
97
  - lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
92
98
  - lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
@@ -116,6 +122,7 @@ files:
116
122
  - lib/rodauth/features/oauth_jwt_bearer_grant.rb
117
123
  - lib/rodauth/features/oauth_management_base.rb
118
124
  - lib/rodauth/features/oauth_pkce.rb
125
+ - lib/rodauth/features/oauth_resource_indicators.rb
119
126
  - lib/rodauth/features/oauth_resource_server.rb
120
127
  - lib/rodauth/features/oauth_saml_bearer_grant.rb
121
128
  - lib/rodauth/features/oauth_token_introspection.rb
@@ -131,6 +138,7 @@ files:
131
138
  - lib/rodauth/oauth/ttl_store.rb
132
139
  - lib/rodauth/oauth/version.rb
133
140
  - locales/en.yml
141
+ - locales/pt.yml
134
142
  - templates/authorize.str
135
143
  - templates/client_secret_field.str
136
144
  - templates/description_field.str