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 +4 -4
- data/README.md +8 -5
- data/doc/release_notes/0_10_0.md +100 -0
- data/doc/release_notes/0_9_2.md +10 -0
- data/doc/release_notes/0_9_3.md +9 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +27 -3
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +5 -0
- data/lib/rodauth/features/oauth_application_management.rb +1 -1
- data/lib/rodauth/features/oauth_base.rb +47 -53
- data/lib/rodauth/features/oauth_jwt.rb +81 -46
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +1 -0
- data/lib/rodauth/features/oauth_management_base.rb +4 -0
- data/lib/rodauth/features/oauth_resource_indicators.rb +153 -0
- data/lib/rodauth/features/oidc.rb +125 -17
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +3 -2
- data/locales/pt.yml +57 -0
- data/templates/authorize.str +13 -3
- data/templates/jwks_field.str +1 -1
- data/templates/jwt_public_key_field.str +1 -1
- data/templates/new_oauth_application.str +1 -1
- data/templates/scope_field.str +3 -2
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9b68ff6e15b91128db72a07fa91b86afb70352f9582fa8c27e7abfe3c0dc17c
|
4
|
+
data.tar.gz: 1c35b67bc10619c8de31cbcef514636e7975307a0cfc02585ae10ec97de74be1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2cf0e357529093b45834697c54bae5eaf17419885e04ccba279d18e65464aa8d8fb2e49da09dd5c96c83331e0f60915e993af5dfc7decfff4c8752b5401dfe8a
|
7
|
+
data.tar.gz: 784d5184526ff8dcbc3c112eb58311705baf82e1bf17b40b87cd55dc43f0b06cc6bf7c2cb71f67caf315008b14d3fe4b9fe8eea991a0475586bf4effb0d77ed3
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Rodauth::Oauth
|
2
2
|
|
3
|
-
[](http://rubygems.org/gems/rodauth-oauth)
|
4
|
+
[](https://gitlab.com/honeyryderchuck/rodauth-oauth/pipelines?page=1&scope=all&ref=master)
|
4
5
|
[](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
|
-
|
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 `
|
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
|
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
|
43
|
-
|
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, "
|
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
|
-
|
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
|
-
|
432
|
-
|
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
|
-
|
438
|
-
|
439
|
-
|
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
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
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
|
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
|
-
|
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
|
-
|
583
|
-
update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
603
|
+
refresh_token = _generate_refresh_token(update_params)
|
584
604
|
else
|
585
|
-
|
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 =
|
589
|
-
|
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
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
191
|
-
|
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
|
-
|
195
|
+
jwt_encode(claims)
|
196
|
+
end
|
200
197
|
|
201
|
-
|
202
|
-
|
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
|
-
|
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
|
-
|
452
|
-
|
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
|
-
|
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,
|
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,
|
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
|
-
|
590
|
-
|
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
|
@@ -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(
|
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
|
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
|
316
|
-
return
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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")
|
597
|
+
scope_claims.unshift("auth_time")
|
490
598
|
|
491
599
|
response_types_supported = metadata[:response_types_supported]
|
492
600
|
|
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
|
-
|
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: "
|
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"
|
data/templates/authorize.str
CHANGED
@@ -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)}"/>
|
data/templates/jwks_field.str
CHANGED
@@ -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
|
-
|
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>
|
data/templates/scope_field.str
CHANGED
@@ -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
|
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.
|
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-
|
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
|