rodauth-oauth 0.9.1 → 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: 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