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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -1
  3. data/doc/release_notes/1_6_6.md +25 -0
  4. data/doc/release_notes/1_6_7.md +17 -0
  5. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +6 -48
  6. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +6 -0
  7. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +3 -1
  8. data/lib/rodauth/features/oauth_application_management.rb +22 -2
  9. data/lib/rodauth/features/oauth_authorization_code_grant.rb +2 -2
  10. data/lib/rodauth/features/oauth_authorize_base.rb +27 -1
  11. data/lib/rodauth/features/oauth_base.rb +64 -17
  12. data/lib/rodauth/features/oauth_device_code_grant.rb +5 -0
  13. data/lib/rodauth/features/oauth_dpop.rb +16 -0
  14. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +71 -33
  15. data/lib/rodauth/features/oauth_grant_management.rb +6 -0
  16. data/lib/rodauth/features/oauth_jwt.rb +1 -1
  17. data/lib/rodauth/features/oauth_jwt_base.rb +56 -13
  18. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +8 -0
  19. data/lib/rodauth/features/oauth_jwt_jwks.rb +6 -0
  20. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +7 -1
  21. data/lib/rodauth/features/oauth_pkce.rb +35 -4
  22. data/lib/rodauth/features/oauth_pushed_authorization_request.rb +6 -0
  23. data/lib/rodauth/features/oauth_resource_indicators.rb +16 -1
  24. data/lib/rodauth/features/oauth_resource_server.rb +1 -1
  25. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +8 -3
  26. data/lib/rodauth/features/oauth_tls_client_auth.rb +14 -2
  27. data/lib/rodauth/features/oauth_token_revocation.rb +2 -1
  28. data/lib/rodauth/features/oidc.rb +35 -19
  29. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +1 -1
  30. data/lib/rodauth/features/oidc_frontchannel_logout.rb +12 -4
  31. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +8 -2
  32. data/lib/rodauth/features/oidc_self_issued.rb +9 -1
  33. data/lib/rodauth/oauth/database_extensions.rb +1 -2
  34. data/lib/rodauth/oauth/ttl_store.rb +3 -3
  35. data/lib/rodauth/oauth/version.rb +1 -1
  36. data/locales/en.yml +4 -0
  37. data/locales/pt.yml +4 -0
  38. data/templates/authorize.str +8 -26
  39. data/templates/device_verification.str +3 -3
  40. data/templates/oauth_application.str +4 -1
  41. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc6a614c797762b13c8b97dc51abf5191dc09ef919e27fd394f46504b62410c7
4
- data.tar.gz: 3c6003d4131e03514a5bf8acd674cc1b7d8fcacd9eada7ab98044fc3f037fc95
3
+ metadata.gz: 47de1ced2a621692058d69609ccb69999440ac8ca3de7f7a43f8522a81b93a20
4
+ data.tar.gz: 4822470b6f682648a772b8d7244000a68557875ee12ebd2126033f58128d9241
5
5
  SHA512:
6
- metadata.gz: 997c1e682fc4f482d0ea367a918c4db15130535294db13f19c52c558f152eca78fd197461d74ce269e77a56b36cf465b8b7e9d2424f3b999cb6491782c8d69f6
7
- data.tar.gz: 2109d08b8911e33697e08e528899d45535de16c1cc994d663324c51161fed1f6e35386849413adb6dadbb526b2b75c64522865cbdf90798042ca8e68767362cf
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 `oauth_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).
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
- <%= hidden_field_tag :client_id, rodauth.param_or_nil("client_id") %>
41
- <% %w[access_type response_type response_mode state redirect_uri].each do |oauth_param| %>
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
- <%= link_to rodauth.oauth_cancel_button, "#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request#{"&state=\#{CGI.escape(rodauth.state)}" if rodauth.param_or_nil("state") }", class: "btn btn-outline-danger" %>
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
- t.string :client_secret, null: false, index: { unique: true }
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
- auth_value_methods(:only_json?)
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 defined?(@oauth_application)
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 defined?(@authorization_token)
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
- oauth_application[oauth_applications_token_endpoint_auth_method_column].split(/ +/)
447
+ supported_auth_methods = if (auth_methods = oauth_application[oauth_applications_token_endpoint_auth_method_column])
448
+ auth_methods.split(/ +/)
409
449
  else
410
- oauth_token_endpoint_auth_methods_supported
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] == secret
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)
@@ -38,6 +38,11 @@ module Rodauth
38
38
  auth_methods(
39
39
  :generate_user_code
40
40
  )
41
+ if respond_to?(:uses_instance_variables)
42
+ uses_instance_variables(
43
+ :@oauth_grant
44
+ )
45
+ end
41
46
 
42
47
  # /device-authorization
43
48
  auth_server_route(:device_authorization) do |r|
@@ -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