rodauth-oauth 1.6.5 → 1.6.7
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 +9 -1
- data/doc/release_notes/1_6_6.md +25 -0
- data/doc/release_notes/1_6_7.md +17 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +6 -48
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +6 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +3 -1
- data/lib/rodauth/features/oauth_application_management.rb +22 -2
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +2 -2
- data/lib/rodauth/features/oauth_authorize_base.rb +27 -1
- data/lib/rodauth/features/oauth_base.rb +64 -17
- data/lib/rodauth/features/oauth_device_code_grant.rb +5 -0
- data/lib/rodauth/features/oauth_dpop.rb +16 -0
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +71 -33
- data/lib/rodauth/features/oauth_grant_management.rb +6 -0
- data/lib/rodauth/features/oauth_jwt.rb +1 -1
- data/lib/rodauth/features/oauth_jwt_base.rb +56 -13
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +8 -0
- data/lib/rodauth/features/oauth_jwt_jwks.rb +6 -0
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +7 -1
- data/lib/rodauth/features/oauth_pkce.rb +35 -4
- data/lib/rodauth/features/oauth_pushed_authorization_request.rb +6 -0
- data/lib/rodauth/features/oauth_resource_indicators.rb +16 -1
- data/lib/rodauth/features/oauth_resource_server.rb +1 -1
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +8 -3
- data/lib/rodauth/features/oauth_tls_client_auth.rb +14 -2
- data/lib/rodauth/features/oauth_token_revocation.rb +2 -1
- data/lib/rodauth/features/oidc.rb +35 -19
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +1 -1
- data/lib/rodauth/features/oidc_frontchannel_logout.rb +12 -4
- data/lib/rodauth/features/oidc_rp_initiated_logout.rb +8 -2
- data/lib/rodauth/features/oidc_self_issued.rb +9 -1
- data/lib/rodauth/oauth/database_extensions.rb +1 -2
- data/lib/rodauth/oauth/ttl_store.rb +3 -3
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +4 -0
- data/locales/pt.yml +4 -0
- data/templates/authorize.str +8 -26
- data/templates/device_verification.str +3 -3
- data/templates/oauth_application.str +4 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47de1ced2a621692058d69609ccb69999440ac8ca3de7f7a43f8522a81b93a20
|
|
4
|
+
data.tar.gz: 4822470b6f682648a772b8d7244000a68557875ee12ebd2126033f58128d9241
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29604d9791ebdab3ea7a85a8ad05cc1ff32aaf8b73aad85b367be9cdd2ab5f199540d25b179147ef5346da7d709fcbf470aac05db6b903d80c483ce47214d399
|
|
7
|
+
data.tar.gz: 7d853b0d1a139fe2d10ec2d865493c8bdf9db313df79e4778e44943900f5634eabb03ac851a65f4f0bc31708014b037116ac46bf150b9fed8bee93fab5f9909c
|
data/README.md
CHANGED
|
@@ -183,7 +183,15 @@ If you're starting from scratch though, the recommendation is to stick to the de
|
|
|
183
183
|
|
|
184
184
|
You'll have to generate HTML templates for the Oauth Authorization form.
|
|
185
185
|
|
|
186
|
-
The rodauth default setup expects the roda `render` plugin to be activated; by default, it expects a `views` directory to be defined in the project root folder. The Oauth Authorization template must be therefore defined there, and it should be called `
|
|
186
|
+
The rodauth default setup expects the roda `render` plugin to be activated; by default, it expects a `views` directory to be defined in the project root folder. The Oauth Authorization template must be therefore defined there, and it should be called `authorize.(erb|str|...)` (read the [roda `render` plugin documentation](http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Render.html) for more info about HTML templating).
|
|
187
|
+
|
|
188
|
+
Start it from [the bundled template](templates/authorize.str), which is written for the `str` engine, rather than from a blank file:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
cp "$(bundle info rodauth-oauth --path)/templates/authorize.str" views/authorize.str
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The form posts back to the authorize endpoint, so every authorization request parameter the grant depends on has to be carried over into it. A parameter left out is dropped from the grant silently, and only surfaces later as an error at the token endpoint, which is why adapting the bundled template beats writing one from scratch. See [the authorize form docs](doc/oauth_authorize_base.rdoc) for the parts which are easiest to lose.
|
|
187
195
|
|
|
188
196
|
### OAuth applications management
|
|
189
197
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
### 1.6.6
|
|
2
|
+
|
|
3
|
+
#### Improvements
|
|
4
|
+
|
|
5
|
+
* Authorization form params will now be exposed to the HTML rendering via the `rodauth.authorize_form_params`; this not only reduces the size of the bundled forms, but also makes it much easier to add or remove features with their own required authorize params.
|
|
6
|
+
* `:oauth_jwt_base` feature: new `:oauth_jwt_iat_leeway` option (defaults to 30, as in seconds), is used now to make the JWT claim `iat` claim tolerant to clock skew. This option is used in both the `jwt` and `json/jwt` gem integrations.
|
|
7
|
+
* `:oauth_pkce` feature: new `:oauth_pkce_allow_plain_method` option, which may disallow the usage of `"plain"` as a PKCE challenge method
|
|
8
|
+
* defaults to `true` in order not to break backwards compatibility, but this is going to change in a future version, so you are recommended to set it explicitly in order not to break your application in a future upgrade.
|
|
9
|
+
|
|
10
|
+
#### Bugfixes
|
|
11
|
+
|
|
12
|
+
* (critical) `:oauth_dynamic_client_registration` feature: `PUT /register` was incorrectly authenticating the request (responding with 200 when secret didn't match), thereby allowing anyone (else) to update application parameters.
|
|
13
|
+
* (critical) `:oauth_dynamic_client_registration` feature: `PUT /register` was not hashing the client secret when hashing on the client secret was enforced.
|
|
14
|
+
* `:oauth_authorization_code_grant` feature: escape untrusted values in the authorize form (hidden request params, the `"state"` in the cancel link, the client application name), in the authorize form post POST response form, and in the device verification template.
|
|
15
|
+
* token endpoint: enforce 4XX HTTP responses with JSON encoded body when there are errors (instead of making it rely on the "accept" HTTP header).
|
|
16
|
+
* client authentication: fallback to `oauth_default_token_endpoint_auth_methods` when the application being authentication has no `token_endpoint_auth_method` set (instead of assuming it as "none").
|
|
17
|
+
* `:oauth_dynamic_client_registration` feature: client metadata which the server does not understand is now ignored (RFC 7591 section 3.1), instead of failing the registration with an `"invalid_client_metadata"` error.
|
|
18
|
+
* `:oauth_dynamic_client_registration` feature: fix typo which was exposing `client_secret` in the registration response in cases where it shouldn't.
|
|
19
|
+
* `:oauth_dynamic_client_registration` feature: emit `"client_id_issued_at"` as epoch seconds instead of an ISO8601 string, as per [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1).
|
|
20
|
+
* `:oauth_dynamic_client_registration` feature: remove "registration_access_token" and "registration_client_uri" parameters from the payload (these are only valid for the OIDC counterpart).
|
|
21
|
+
* `json/jwt`: decoding a JWT was bypassing claims verification.
|
|
22
|
+
|
|
23
|
+
#### Security
|
|
24
|
+
|
|
25
|
+
* Using `rodauth` `timing_safe_eql?` helper method, a string comparison function which is safe against timing attacks, in client secret matching and PKCE verification.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
### 1.6.7
|
|
2
|
+
|
|
3
|
+
#### Improvements
|
|
4
|
+
|
|
5
|
+
* Some new capabilities were built into `rodauth` recently, which `rodauth-oauth` now taps into:
|
|
6
|
+
* plugins are now object shape friendly, by defining ivars using `rodauth`'s `:uses_instance_variables` auth method (introduced in v2.44)
|
|
7
|
+
* `:oidc` plugin `get_oidc_param` and `get_additional_param` now have a default implementation, which raises an error (eliminates warnings).
|
|
8
|
+
* `only_json?` auth method is no longer defined (unless the `:jwt` plugin is used).
|
|
9
|
+
* some internal methods were now moved to private, instead of being needlessly exposed (eliminates security warnings).
|
|
10
|
+
* a new auth method, `confidential?(oauth_application)`, was added to the `:oauth_base` plugin, which is now used internally to check in some key flows whether the OAuth client application is a public or a confidential client, as it's defined in the OAuth RFC:
|
|
11
|
+
* when using the `:oauth_dynamic_client_registration` plugin, a client secret won't be generated for public clients (same logic will be applied for client registration management endpoints).
|
|
12
|
+
* when using the `:oauth_application_management` plugin, default templates will include a client type column, and omit the client secret for public clients, where they would previously.
|
|
13
|
+
|
|
14
|
+
#### Bugfixes
|
|
15
|
+
|
|
16
|
+
* do not render null fields in the payload of the oauth server metadata endpoint (RFC 8414 section 2 requires omission of undefined values, so clients can apply the recommended defaults).
|
|
17
|
+
* fixed regexp used for json requests (same fix as in [rodauth](https://github.com/jeremyevans/rodauth/commit/3e0d7ab2d49a5733d1afcaaf1062b8a8258aa57a))
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<%= image_tag rodauth.oauth_application[rodauth.oauth_applications_logo_uri_column] %>
|
|
4
4
|
<% end %>
|
|
5
5
|
<% application_uri = rodauth.oauth_application[rodauth.oauth_applications_homepage_url_column] %>
|
|
6
|
-
<% application_name = application_uri ? link_to(rodauth.oauth_application[rodauth.oauth_applications_name_column], application_uri) : rodauth.oauth_application[rodauth.oauth_applications_name_column] %>
|
|
6
|
+
<% application_name = application_uri ? link_to(rodauth.oauth_application[rodauth.oauth_applications_name_column], application_uri) : h(rodauth.oauth_application[rodauth.oauth_applications_name_column]) %>
|
|
7
7
|
<p class="lead"><%= rodauth.authorize_page_lead(name: application_name).html_safe %></p>
|
|
8
8
|
|
|
9
9
|
<div class="list-group">
|
|
@@ -37,56 +37,14 @@
|
|
|
37
37
|
</div>
|
|
38
38
|
<% end %>
|
|
39
39
|
<% end %>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<% if rodauth.param_or_nil(oauth_param) %>
|
|
43
|
-
<%= hidden_field_tag oauth_param, rodauth.param_or_nil(oauth_param) %>
|
|
44
|
-
<% end %>
|
|
45
|
-
<% end %>
|
|
46
|
-
<% if rodauth.features.include?(:oauth_resource_indicators) && rodauth.resource_indicators %>
|
|
47
|
-
<% rodauth.resource_indicators.each do |resource| %>
|
|
48
|
-
<%= hidden_field_tag "resource", resource %>
|
|
49
|
-
<% end %>
|
|
50
|
-
<% end %>
|
|
51
|
-
<% if rodauth.features.include?(:oauth_pkce) %>
|
|
52
|
-
<% if rodauth.param_or_nil("code_challenge") %>
|
|
53
|
-
<%= hidden_field_tag :code_challenge, rodauth.param_or_nil("code_challenge") %>
|
|
54
|
-
<% end %>
|
|
55
|
-
<% if rodauth.param_or_nil("code_challenge_method") %>
|
|
56
|
-
<%= hidden_field_tag :code_challenge_method, rodauth.param_or_nil("code_challenge_method") %>
|
|
57
|
-
<% end %>
|
|
58
|
-
<% end %>
|
|
59
|
-
<% if rodauth.features.include?(:oidc) %>
|
|
60
|
-
<% if rodauth.param_or_nil("prompt") %>
|
|
61
|
-
<%= hidden_field_tag :prompt, rodauth.param_or_nil("prompt") %>
|
|
62
|
-
<% end %>
|
|
63
|
-
<% if rodauth.param_or_nil("nonce") %>
|
|
64
|
-
<%= hidden_field_tag :nonce, rodauth.param_or_nil("nonce") %>
|
|
65
|
-
<% end %>
|
|
66
|
-
<% if rodauth.param_or_nil("ui_locales") %>
|
|
67
|
-
<%= hidden_field_tag :ui_locales, rodauth.param_or_nil("ui_locales") %>
|
|
68
|
-
<% end %>
|
|
69
|
-
<% if rodauth.param_or_nil("claims_locales") %>
|
|
70
|
-
<%= hidden_field_tag :claims_locales, rodauth.param_or_nil("claims_locales") %>
|
|
71
|
-
<% end %>
|
|
72
|
-
<% if rodauth.param_or_nil("claims") %>
|
|
73
|
-
<%= hidden_field_tag :claims, sanitize(rodauth.param_or_nil("claims")) %>
|
|
74
|
-
<% end %>
|
|
75
|
-
<% if rodauth.param_or_nil("acr_values") %>
|
|
76
|
-
<%= hidden_field_tag :acr_values, rodauth.param_or_nil("acr_values") %>
|
|
77
|
-
<% end %>
|
|
78
|
-
<% if rodauth.param_or_nil("registration") %>
|
|
79
|
-
<%= hidden_field_tag :registration, rodauth.param_or_nil("registration") %>
|
|
80
|
-
<% end %>
|
|
81
|
-
<% end %>
|
|
82
|
-
<% if rodauth.features.include?(:oauth_dpop) %>
|
|
83
|
-
<% if rodauth.param_or_nil("dpop_jkt") %>
|
|
84
|
-
<%= hidden_field_tag :dpop_jkt, rodauth.param_or_nil("dpop_jkt") %>
|
|
85
|
-
<% end %>
|
|
40
|
+
<% rodauth.authorize_form_params.each do |field| %>
|
|
41
|
+
<%= tag(:input, type: field["type"], name: field["name"], value: field["value"]) %>
|
|
86
42
|
<% end %>
|
|
87
43
|
</div>
|
|
88
44
|
<p class="text-center">
|
|
89
45
|
<%= submit_tag rodauth.oauth_authorize_button, class: "btn btn-outline-primary" %>
|
|
90
|
-
|
|
46
|
+
<% cancel_url = "#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request" %>
|
|
47
|
+
<% cancel_url += "&state=#{CGI.escape(rodauth.param("state"))}" if rodauth.param_or_nil("state") %>
|
|
48
|
+
<%= link_to rodauth.oauth_cancel_button, cancel_url, class: "btn btn-outline-danger" %>
|
|
91
49
|
</p>
|
|
92
50
|
<% end %>
|
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
<dd><%= oauth_application[rodauth.oauth_applications_description_column] %></dd>
|
|
8
8
|
<dt><%= rodauth.oauth_applications_homepage_url_label %>: </dt>
|
|
9
9
|
<dd><%= oauth_application[rodauth.oauth_applications_homepage_url_column] %></dd>
|
|
10
|
+
<dt><%= rodauth.oauth_applications_client_type_label %>: </dt>
|
|
11
|
+
<dd><%= rodauth.oauth_application_client_type_label(oauth_application) %></dd>
|
|
10
12
|
<dt><%= rodauth.oauth_applications_client_id_label %>: </dt>
|
|
11
13
|
<dd><%= oauth_application[rodauth.oauth_applications_client_id_column] %></dd>
|
|
14
|
+
<% if rodauth.confidential?(oauth_application) %>
|
|
15
|
+
<dt><%= rodauth.oauth_applications_client_secret_label %>: </dt>
|
|
16
|
+
<dd><%= oauth_application[rodauth.oauth_applications_client_secret_column] %></dd>
|
|
17
|
+
<% end %>
|
|
12
18
|
<dt><%= rodauth.oauth_applications_redirect_uri_label %>: </dt>
|
|
13
19
|
<dd><%= oauth_application[rodauth.oauth_applications_redirect_uri_column] %></dd>
|
|
14
20
|
<dt><%= rodauth.oauth_applications_scopes_label %>: </dt>
|
|
@@ -8,7 +8,9 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
|
|
8
8
|
t.string :homepage_url, null: true
|
|
9
9
|
t.string :redirect_uri, null: false
|
|
10
10
|
t.string :client_id, null: false, index: { unique: true }
|
|
11
|
-
|
|
11
|
+
# change `:null` contraints to false if you want to support confidential clients only, and
|
|
12
|
+
# no scheme relying on JWT private keys advertised by the JWKs uri.
|
|
13
|
+
t.string :client_secret, null: true, index: { unique: true }
|
|
12
14
|
t.string :registration_access_token, null: true
|
|
13
15
|
t.string :scopes, null: false
|
|
14
16
|
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP(6)" }
|
|
@@ -35,8 +35,11 @@ module Rodauth
|
|
|
35
35
|
translatable_method :oauth_applications_jwks_uri_label, "JSON Web Keys URI"
|
|
36
36
|
translatable_method :oauth_applications_homepage_url_label, "Homepage URL"
|
|
37
37
|
translatable_method :oauth_applications_redirect_uri_label, "Redirect URI"
|
|
38
|
-
translatable_method :oauth_applications_client_secret_label, "Client Secret"
|
|
39
38
|
translatable_method :oauth_applications_client_id_label, "Client ID"
|
|
39
|
+
translatable_method :oauth_applications_client_secret_label, "Client Secret"
|
|
40
|
+
translatable_method :oauth_applications_client_type_label, "Client Type"
|
|
41
|
+
translatable_method :oauth_applications_confidential_label, "Confidential"
|
|
42
|
+
translatable_method :oauth_applications_public_label, "Public"
|
|
40
43
|
|
|
41
44
|
%w[type token refresh_token expires_in revoked_at].each do |param|
|
|
42
45
|
translatable_method :"oauth_grants_#{param}_label", param.gsub("_", " ").capitalize
|
|
@@ -58,9 +61,18 @@ module Rodauth
|
|
|
58
61
|
translatable_method :oauth_no_grants_text, "No oauth grants yet!"
|
|
59
62
|
|
|
60
63
|
auth_methods(
|
|
61
|
-
:oauth_application_path
|
|
64
|
+
:oauth_application_path,
|
|
65
|
+
:oauth_application_client_type_label
|
|
62
66
|
)
|
|
63
67
|
|
|
68
|
+
if respond_to?(:uses_instance_variables)
|
|
69
|
+
uses_instance_variables(
|
|
70
|
+
:@oauth_applications,
|
|
71
|
+
:@oauth_grants,
|
|
72
|
+
:@field_errors
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
64
76
|
def oauth_applications_path(opts = {})
|
|
65
77
|
route_path(oauth_applications_route, opts)
|
|
66
78
|
end
|
|
@@ -69,6 +81,14 @@ module Rodauth
|
|
|
69
81
|
"#{oauth_applications_path}/#{id}"
|
|
70
82
|
end
|
|
71
83
|
|
|
84
|
+
def oauth_application_client_type_label(oauth_application)
|
|
85
|
+
if confidential?(oauth_application)
|
|
86
|
+
oauth_applications_confidential_label
|
|
87
|
+
else
|
|
88
|
+
oauth_applications_public_label
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
72
92
|
# /oauth-applications routes
|
|
73
93
|
def load_oauth_application_management_routes
|
|
74
94
|
request.on(oauth_applications_route) do
|
|
@@ -116,7 +116,7 @@ module Rodauth
|
|
|
116
116
|
<html>
|
|
117
117
|
<head><title>Authorized</title></head>
|
|
118
118
|
<body onload="javascript:document.forms[0].submit()">
|
|
119
|
-
<form method="post" action="#{url}">
|
|
119
|
+
<form method="post" action="#{scope.h(url)}">
|
|
120
120
|
#{yield}
|
|
121
121
|
<input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
|
|
122
122
|
</form>
|
|
@@ -130,7 +130,7 @@ module Rodauth
|
|
|
130
130
|
<html>
|
|
131
131
|
<head><title></title></head>
|
|
132
132
|
<body onload="javascript:document.forms[0].submit()">
|
|
133
|
-
<form method="post" action="#{url}">
|
|
133
|
+
<form method="post" action="#{scope.h(url)}">
|
|
134
134
|
#{yield}
|
|
135
135
|
</form>
|
|
136
136
|
</body>
|
|
@@ -30,9 +30,17 @@ module Rodauth
|
|
|
30
30
|
|
|
31
31
|
auth_methods(
|
|
32
32
|
:resource_owner_params,
|
|
33
|
-
:oauth_grants_resource_owner_columns
|
|
33
|
+
:oauth_grants_resource_owner_columns,
|
|
34
|
+
:authorize_form_params
|
|
34
35
|
)
|
|
35
36
|
|
|
37
|
+
if respond_to?(:uses_instance_variables)
|
|
38
|
+
uses_instance_variables(
|
|
39
|
+
:@error,
|
|
40
|
+
:@back_url
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
36
44
|
OAUTH_ACCESS_TYPES = %w[offline online].freeze
|
|
37
45
|
|
|
38
46
|
OAUTH_APPROVAL_PROMPTS = %w[force auto].freeze
|
|
@@ -73,6 +81,24 @@ module Rodauth
|
|
|
73
81
|
end
|
|
74
82
|
end
|
|
75
83
|
|
|
84
|
+
# The authorization request is validated on the GET which renders the form, and again on the
|
|
85
|
+
# POST which the resource owner submits, and the latter is a fresh request. These are the
|
|
86
|
+
# params of the authorization request which the form has to carry over into it, one entry per
|
|
87
|
+
# field, each with the "name", "value" and "type" it is rendered with. Features which introduce
|
|
88
|
+
# authorization request params of their own extend it, so a view which renders all of them
|
|
89
|
+
# keeps working as features get enabled.
|
|
90
|
+
def authorize_form_params
|
|
91
|
+
params = [{ "name" => "client_id", "value" => param("client_id"), "type" => "hidden" }]
|
|
92
|
+
|
|
93
|
+
%w[access_type response_type response_mode state redirect_uri].each do |param_name|
|
|
94
|
+
if (param_value = param_or_nil(param_name))
|
|
95
|
+
params << { "name" => param_name, "value" => param_value, "type" => "hidden" }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
params
|
|
100
|
+
end
|
|
101
|
+
|
|
76
102
|
private
|
|
77
103
|
|
|
78
104
|
def validate_authorize_params
|
|
@@ -34,6 +34,15 @@ module Rodauth
|
|
|
34
34
|
auth_value_method :oauth_unique_id_generation_retries, 3
|
|
35
35
|
|
|
36
36
|
auth_value_method :oauth_token_endpoint_auth_methods_supported, %w[client_secret_basic client_secret_post]
|
|
37
|
+
# An application which has no auth method of its own falls back to these, and not to every
|
|
38
|
+
# method the server supports: enabling a new method server-wide (such as "none", which requires
|
|
39
|
+
# no client authentication at all) must not turn existing confidential clients into public ones.
|
|
40
|
+
#
|
|
41
|
+
# TODO: change this to "client_secret_basic" only in a future major version, as per
|
|
42
|
+
# https://datatracker.ietf.org/doc/html/rfc7591#section-2, which defaults an omitted
|
|
43
|
+
# "token_endpoint_auth_method" to it. Doing so now would lock out "client_secret_post" clients
|
|
44
|
+
# whose application row has no auth method set.
|
|
45
|
+
auth_value_method :oauth_default_token_endpoint_auth_methods, %w[client_secret_basic client_secret_post]
|
|
37
46
|
auth_value_method :oauth_grant_types_supported, %w[refresh_token]
|
|
38
47
|
auth_value_method :oauth_response_types_supported, []
|
|
39
48
|
auth_value_method :oauth_response_modes_supported, []
|
|
@@ -67,7 +76,7 @@ module Rodauth
|
|
|
67
76
|
account_id
|
|
68
77
|
name description scopes
|
|
69
78
|
client_id client_secret
|
|
70
|
-
homepage_url redirect_uri
|
|
79
|
+
confidential homepage_url redirect_uri
|
|
71
80
|
token_endpoint_auth_method grant_types response_types response_modes
|
|
72
81
|
logo_uri tos_uri policy_uri jwks jwks_uri
|
|
73
82
|
contacts software_id software_version
|
|
@@ -98,9 +107,7 @@ module Rodauth
|
|
|
98
107
|
|
|
99
108
|
auth_value_method :is_authorization_server?, true
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
|
|
110
|
+
auth_value_method :json_request_regexp, %r{\Aapplication/(?:vnd\.api\+)?json\b}i
|
|
104
111
|
|
|
105
112
|
# METADATA
|
|
106
113
|
auth_value_method :oauth_metadata_service_documentation, nil
|
|
@@ -121,9 +128,18 @@ module Rodauth
|
|
|
121
128
|
:oauth_unique_id_generator,
|
|
122
129
|
:require_authorizable_account,
|
|
123
130
|
:oauth_account_ds,
|
|
124
|
-
:oauth_application_ds
|
|
131
|
+
:oauth_application_ds,
|
|
132
|
+
:confidential?
|
|
125
133
|
)
|
|
126
134
|
|
|
135
|
+
if respond_to?(:uses_instance_variables)
|
|
136
|
+
uses_instance_variables(
|
|
137
|
+
:@json_request,
|
|
138
|
+
:@oauth_application,
|
|
139
|
+
:@authorization_token
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
127
143
|
# /token
|
|
128
144
|
auth_server_route(:token) do |r|
|
|
129
145
|
require_oauth_application
|
|
@@ -188,6 +204,11 @@ module Rodauth
|
|
|
188
204
|
def accepts_json?
|
|
189
205
|
return true if only_json?
|
|
190
206
|
|
|
207
|
+
# the token endpoint responds in json no matter what the client asks for, including its
|
|
208
|
+
# errors, which are never bounced off the client's redirect uri.
|
|
209
|
+
# https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
|
210
|
+
return true if request.path == token_path
|
|
211
|
+
|
|
191
212
|
(accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
|
|
192
213
|
end
|
|
193
214
|
|
|
@@ -219,7 +240,7 @@ module Rodauth
|
|
|
219
240
|
end
|
|
220
241
|
|
|
221
242
|
def oauth_application
|
|
222
|
-
return @oauth_application if
|
|
243
|
+
return @oauth_application if @oauth_application
|
|
223
244
|
|
|
224
245
|
client_id = param_or_nil("client_id")
|
|
225
246
|
|
|
@@ -228,6 +249,21 @@ module Rodauth
|
|
|
228
249
|
@oauth_application = db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first
|
|
229
250
|
end
|
|
230
251
|
|
|
252
|
+
def confidential?(oauth_application)
|
|
253
|
+
token_endpoint_auth_method = oauth_application[oauth_applications_token_endpoint_auth_method_column]
|
|
254
|
+
|
|
255
|
+
# https://datatracker.ietf.org/doc/html/rfc7591#section-2
|
|
256
|
+
# "none": The client is a public client as defined in OAuth 2.0, Section 2.1, and does not have a client secret.
|
|
257
|
+
return false if token_endpoint_auth_method == "none"
|
|
258
|
+
|
|
259
|
+
oauth_confidential_token_endpoint_auth_methods.include?(token_endpoint_auth_method) ||
|
|
260
|
+
!(
|
|
261
|
+
# not exclusively registering implicit grant
|
|
262
|
+
Array(oauth_application[oauth_applications_grant_types_column]).include?("implicit") &&
|
|
263
|
+
Array(oauth_application[oauth_applications_response_types_column]).include?("token")
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
|
|
231
267
|
def fetch_access_token
|
|
232
268
|
if (token = request.params["access_token"])
|
|
233
269
|
if request.post? && !(request.content_type.start_with?("application/x-www-form-urlencoded") &&
|
|
@@ -256,7 +292,7 @@ module Rodauth
|
|
|
256
292
|
end
|
|
257
293
|
|
|
258
294
|
def authorization_token
|
|
259
|
-
return @authorization_token if
|
|
295
|
+
return @authorization_token if @authorization_token
|
|
260
296
|
|
|
261
297
|
# check if there is a token
|
|
262
298
|
access_token = fetch_access_token
|
|
@@ -315,6 +351,10 @@ module Rodauth
|
|
|
315
351
|
|
|
316
352
|
private
|
|
317
353
|
|
|
354
|
+
def oauth_confidential_token_endpoint_auth_methods
|
|
355
|
+
%w[client_secret_basic client_secret_post]
|
|
356
|
+
end
|
|
357
|
+
|
|
318
358
|
def oauth_account_ds(account_id)
|
|
319
359
|
account_ds(account_id)
|
|
320
360
|
end
|
|
@@ -404,10 +444,10 @@ module Rodauth
|
|
|
404
444
|
def supports_auth_method?(oauth_application, auth_method)
|
|
405
445
|
return false unless oauth_application
|
|
406
446
|
|
|
407
|
-
supported_auth_methods = if oauth_application[oauth_applications_token_endpoint_auth_method_column]
|
|
408
|
-
|
|
447
|
+
supported_auth_methods = if (auth_methods = oauth_application[oauth_applications_token_endpoint_auth_method_column])
|
|
448
|
+
auth_methods.split(/ +/)
|
|
409
449
|
else
|
|
410
|
-
|
|
450
|
+
oauth_default_token_endpoint_auth_methods
|
|
411
451
|
end
|
|
412
452
|
|
|
413
453
|
supported_auth_methods.include?(auth_method)
|
|
@@ -431,7 +471,11 @@ module Rodauth
|
|
|
431
471
|
if oauth_applications_client_secret_hash_column
|
|
432
472
|
BCrypt::Password.new(oauth_application[oauth_applications_client_secret_hash_column]) == secret
|
|
433
473
|
else
|
|
434
|
-
oauth_application[oauth_applications_client_secret_column]
|
|
474
|
+
stored_secret = oauth_application[oauth_applications_client_secret_column]
|
|
475
|
+
# A missing submitted or stored secret must never match. Without these guards, .to_s would
|
|
476
|
+
# coerce nil to "", letting an absent secret authenticate a client whose stored secret is
|
|
477
|
+
# empty/NULL. The real comparison stays constant-time via timing_safe_eql?.
|
|
478
|
+
!secret.nil? && !stored_secret.nil? && timing_safe_eql?(secret.to_s, stored_secret.to_s)
|
|
435
479
|
end
|
|
436
480
|
end
|
|
437
481
|
|
|
@@ -745,19 +789,22 @@ module Rodauth
|
|
|
745
789
|
issuer = base_url
|
|
746
790
|
issuer += "/#{path}" if path
|
|
747
791
|
|
|
748
|
-
{
|
|
792
|
+
payload = {
|
|
749
793
|
issuer: issuer,
|
|
750
794
|
token_endpoint: token_url,
|
|
751
795
|
scopes_supported: oauth_application_scopes,
|
|
752
796
|
response_types_supported: oauth_response_types_supported,
|
|
753
797
|
response_modes_supported: oauth_response_modes_supported,
|
|
754
798
|
grant_types_supported: oauth_grant_types_supported,
|
|
755
|
-
token_endpoint_auth_methods_supported: oauth_token_endpoint_auth_methods_supported
|
|
756
|
-
service_documentation: oauth_metadata_service_documentation,
|
|
757
|
-
ui_locales_supported: oauth_metadata_ui_locales_supported,
|
|
758
|
-
op_policy_uri: oauth_metadata_op_policy_uri,
|
|
759
|
-
op_tos_uri: oauth_metadata_op_tos_uri
|
|
799
|
+
token_endpoint_auth_methods_supported: oauth_token_endpoint_auth_methods_supported
|
|
760
800
|
}
|
|
801
|
+
|
|
802
|
+
# RFC 8414, section 2: unused optional metadata parameters must be omitted, not null
|
|
803
|
+
payload[:service_documentation] = oauth_metadata_service_documentation if oauth_metadata_service_documentation
|
|
804
|
+
payload[:ui_locales_supported] = oauth_metadata_ui_locales_supported if oauth_metadata_ui_locales_supported
|
|
805
|
+
payload[:op_policy_uri] = oauth_metadata_op_policy_uri if oauth_metadata_op_policy_uri
|
|
806
|
+
payload[:op_tos_uri] = oauth_metadata_op_tos_uri if oauth_metadata_op_tos_uri
|
|
807
|
+
payload
|
|
761
808
|
end
|
|
762
809
|
|
|
763
810
|
def redirect_response_error(error_code, message = nil)
|
|
@@ -54,6 +54,14 @@ module Rodauth
|
|
|
54
54
|
|
|
55
55
|
auth_methods(:validate_dpop_proof_usage)
|
|
56
56
|
|
|
57
|
+
if respond_to?(:uses_instance_variables)
|
|
58
|
+
uses_instance_variables(
|
|
59
|
+
:@dpop_access_token,
|
|
60
|
+
:@dpop_claims,
|
|
61
|
+
:@dpop_thumbprint
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
57
65
|
def require_oauth_authorization(*scopes)
|
|
58
66
|
@dpop_access_token = fetch_access_token_from_authorization_header("dpop")
|
|
59
67
|
|
|
@@ -85,6 +93,14 @@ module Rodauth
|
|
|
85
93
|
super
|
|
86
94
|
end
|
|
87
95
|
|
|
96
|
+
def authorize_form_params
|
|
97
|
+
super.tap do |params|
|
|
98
|
+
if (param_value = param_or_nil("dpop_jkt"))
|
|
99
|
+
params << { "name" => "dpop_jkt", "value" => param_value, "type" => "hidden" }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
88
104
|
private
|
|
89
105
|
|
|
90
106
|
def validate_token_params
|