rodauth-oauth 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12c86242a8a2001fba629cb6bd8e25886b8805fce5d0965ebc70377824e25e91
4
- data.tar.gz: 2fdc78f81b737c9c0d0086f258a86807f8855d3f0bc9be89bffb6d6a90946ed3
3
+ metadata.gz: aa95c4db43b0068e5a7c1f8af1d904a26351a007f203be233803bae0ed471f32
4
+ data.tar.gz: fe0da3df41a98b6411fd20378eb41f22e5c4766cf42fd3061a45d0c6b8606bcf
5
5
  SHA512:
6
- metadata.gz: be15b77c46a135d213cd6e6e6bc7000b961e036febdb8f75bb922f09554d68ab757d5094fe96816c83c5966a3094daa32ecbbee798b2a8bb72417df9506ac3b3
7
- data.tar.gz: a5fec610e9193d449ef49fbfde36688398c9e4e576da5ea579165c306ecc51bc910d0d758c923a9bece59ef4e7651347f9ebb7951504f3354b101fe5f845173a
6
+ metadata.gz: 910bb63ff3a0be7cb035d54ed4eb48916b2137d886523403154d0eefd5d72bdb4375a268e8c94eb8047592bddfb2dd79b7022611243e18902ec33f467450e013
7
+ data.tar.gz: 332125982a596d3ea0c773c7e84e5a4e4cdeed2bc9de0069f742d9b2a10de0b2182fcaea69c658fefe88b1a67e4c8b81c2dbafa3dc4c155d0100a64b1d444bb9
data/README.md CHANGED
@@ -24,7 +24,7 @@ This is an extension to the `rodauth` gem which implements the [OAuth 2.0 framew
24
24
 
25
25
  This gem implements the following RFCs and features of OAuth:
26
26
 
27
- * `oauth` - [The OAuth 2.0 protocol framework](https://tools.ietf.org/html/rfc6749):
27
+ * `oauth` - [The OAuth 2.0 protocol framework](-/wikis/home#oauth-20-protocol-framework):
28
28
  * [Access Token generation](https://tools.ietf.org/html/rfc6749#section-1.4);
29
29
  * [Access Token refresh token grant](https://tools.ietf.org/html/rfc6749#section-1.5);
30
30
  * `oauth_authorization_code_grant` - [Authorization code grant](https://tools.ietf.org/html/rfc6749#section-1.3);
@@ -33,8 +33,10 @@ This gem implements the following RFCs and features of OAuth:
33
33
  * `oauth_device_code_grant` - [Device code grant (off by default)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-device-flow-15);
34
34
  * `oauth_token_revocation` - [Token revocation](https://tools.ietf.org/html/rfc7009);
35
35
  * `oauth_token_introspection` - [Token introspection](https://tools.ietf.org/html/rfc7662);
36
+ * `oauth_pushed_authorization_request` - [Pushed Authorization Request](https://datatracker.ietf.org/doc/html/rfc9126);
36
37
  * [Authorization Server Metadata](https://tools.ietf.org/html/rfc8414);
37
38
  * `oauth_pkce` - [PKCE](https://tools.ietf.org/html/rfc7636);
39
+ * `oauth_tls_client_auth` - [Mutual-TLS Client Authentication](https://datatracker.ietf.org/doc/html/rfc8705);
38
40
  * `oauth_jwt` - [JWT Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07);
39
41
  * `oauth_jwt_secured_authorization_request` - [JWT Secured Authorization Request](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
40
42
  * `oauth_resource_indicators` - [Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707);
@@ -44,18 +46,17 @@ This gem implements the following RFCs and features of OAuth:
44
46
  * `oauth_saml_bearer_grant` - [SAML 2.0 Bearer Assertion](https://datatracker.ietf.org/doc/html/rfc7522);
45
47
  * `oauth_jwt_bearer_grant` - [JWT Bearer Assertion](https://datatracker.ietf.org/doc/html/rfc7523);
46
48
 
47
- * `oauth_dynamic_client_registration` - [Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591);
49
+ * `oauth_dynamic_client_registration` - [Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591) and [Dynamic Client Registration Management](https://www.rfc-editor.org/rfc/rfc7592);
48
50
  * OAuth application and token management dashboards;
49
51
  * The recommendations for [Native Apps](https://www.rfc-editor.org/rfc/rfc8252);
50
52
 
51
53
  It also implements the [OpenID Connect layer](https://openid.net/connect/) (via the `openid` feature) on top of the OAuth features it provides, including:
52
54
 
53
- * `oidc`
54
- * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html);
55
- * [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0-29.html);
56
- * [OpenID Multiple Response Types](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html);
57
- * `oidc_dynamic_client_registration` - [OpenID Connect Dynamic Client Registration](https://openid.net/specs/openid-connect-registration-1_0.html);
58
- * `oidc_rp_initiated_logout` - [RP Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html);
55
+ * [OpenID Connect Core](https://gitlab.com/os85/rodauth-oauth/-/wikis/Id-Token-Authentication);
56
+ * [OpenID Connect Discovery](https://gitlab.com/os85/rodauth-oauth/-/wikis/OIDC-Dynamic-Client-Registration);
57
+ * [OpenID Multiple Response Types](https://gitlab.com/os85/rodauth-oauth/-/wikis/Hybrid-flow);
58
+ * [OpenID Connect Dynamic Client Registration](https://gitlab.com/os85/rodauth-oauth/-/wikis/OIDC-Dynamic-Client-Registration);
59
+ * [RP Initiated Logout](https://gitlab.com/os85/rodauth-oauth/-/wikis/RP-Initiated-Logout);
59
60
 
60
61
  This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
61
62
 
@@ -0,0 +1,36 @@
1
+ ## 1.2.0 (13/02/2023)
2
+
3
+ ### Features
4
+
5
+ #### Pushed Authorization Requests (PAR)
6
+
7
+ RFC: https://datatracker.ietf.org/doc/html/rfc9126
8
+
9
+ `rodauth-oauth` supports Pushed Authorization Requests, via the `:oauth_pushed_authorization_request` feature.
10
+
11
+ More info about the feature [in the wiki](https://gitlab.com/os85/rodauth-oauth/-/wikis/Pushed-Authorization-Requests).
12
+
13
+ #### mTLS Client Auth (+ certificate-bound access tokens)
14
+
15
+ RFC: https://www.rfc-editor.org/rfc/rfc8705
16
+
17
+ The `:oauth_tls_client_auth` feature adds support for the variants of mTLS Client Authentication "PKI Mutual-TLS Method" and 2Self-Signed Certificate Mutual-TLS Method". It also supports client certificate bound access tokens.
18
+
19
+ More about it [in the wiki](https://gitlab.com/os85/rodauth-oauth/-/wikis/mTLS-Client-Authentication).
20
+
21
+ #### Dynamic Client Registration management
22
+
23
+ RFC: https://www.rfc-editor.org/rfc/rfc7592
24
+
25
+ Support for dynamci client registration management was added to the `:oauth_dynamic_client_registration` feature.
26
+
27
+ More info about it [in the wiki](https://gitlab.com/os85/rodauth-oauth/-/wikis/Dynamic-Client-Registration#getputdelete-registerclient_id).
28
+
29
+ ### Improvements
30
+
31
+ * Support for 3rd-party initiated login was added, by including support for the `initiate_login_uri` attribute in the register route from the `:oauth_dynamic_client_registration` feature.
32
+ * Support for multitenant resource ownership was added, here's a [description from the wiki](https://gitlab.com/os85/rodauth-oauth/-/wikis/How-to#scoping-grants-from-the-same-resource-owner).
33
+
34
+ ### Bugfixes
35
+
36
+ * oidc: userinfo claims were not including claims with value `false`, such as `"email_verified"`. This behaviour has been fixed, and only claims of value `null` are omitted.
@@ -9,6 +9,7 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
9
9
  t.string :redirect_uri, null: false
10
10
  t.string :client_id, null: false, index: { unique: true }
11
11
  t.string :client_secret, null: false, index: { unique: true }
12
+ t.string :registration_access_token, null: true
12
13
  t.string :scopes, null: false
13
14
  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
14
15
 
@@ -29,6 +30,7 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
29
30
  # :oidc_dynamic_client_configuration enabled, extra optional params
30
31
  t.string :sector_identifier_uri, null: true
31
32
  t.string :application_type, null: true
33
+ t.string :initiate_login_uri, null: true
32
34
 
33
35
  # :oidc enabled
34
36
  t.string :subject_type, null: true
@@ -44,6 +46,15 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
44
46
  t.string :request_object_encryption_alg, null: true
45
47
  t.string :request_object_encryption_enc, null: true
46
48
  t.string :request_uris, null: true
49
+ t.boolean :require_pushed_authorization_requests, null: false, default: false
50
+
51
+ # :oauth_tls_client_auth
52
+ t.string :tls_client_auth_subject_dn, null: true
53
+ t.string :tls_client_auth_san_dns, null: true
54
+ t.string :tls_client_auth_san_uri, null: true
55
+ t.string :tls_client_auth_san_ip, null: true
56
+ t.string :tls_client_auth_san_email, null: true
57
+ t.boolean :tls_client_certificate_bound_access_tokens, default: false
47
58
 
48
59
  # :oidc_rp_initiated_logout enabled
49
60
  t.string :post_logout_redirect_uris, null: false
@@ -74,6 +85,9 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
74
85
  t.string :user_code, null: true, unique: true
75
86
  t.datetime :last_polled_at, null: true
76
87
 
88
+ # :oauth_tls_client_auth
89
+ t.string :certificate_thumbprint, null: true
90
+
77
91
  # :resource_indicators enabled
78
92
  t.string :resource
79
93
 
@@ -83,5 +97,13 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
83
97
  t.string :claims_locales
84
98
  t.string :claims
85
99
  end
100
+
101
+ create_table :oauth_pushed_requests do |t|
102
+ t.integer :oauth_application_id
103
+ t.foreign_key :oauth_applications, column: :oauth_application_id
104
+ t.string :params, null: false
105
+ t.datetime :expires_in, null: false
106
+ t.index %i[oauth_application_id code], unique: true
107
+ end
86
108
  end
87
109
  end
@@ -57,7 +57,7 @@ module Rodauth
57
57
  def _do_authorize_code
58
58
  create_params = {
59
59
  oauth_grants_type_column => "authorization_code",
60
- oauth_grants_account_id_column => account_id
60
+ **resource_owner_params
61
61
  }
62
62
 
63
63
  { "code" => create_oauth_grant(create_params) }
@@ -28,6 +28,11 @@ module Rodauth
28
28
  translatable_method :oauth_unsupported_response_type_message, "Unsupported response type"
29
29
  translatable_method :oauth_authorize_parameter_required, "Invalid or missing '%<parameter>s'"
30
30
 
31
+ auth_value_methods(
32
+ :resource_owner_params,
33
+ :oauth_grants_resource_owner_columns
34
+ )
35
+
31
36
  # /authorize
32
37
  auth_server_route(:authorize) do |r|
33
38
  require_authorizable_account
@@ -73,7 +78,9 @@ module Rodauth
73
78
 
74
79
  if (redirect_uri = param_or_nil("redirect_uri"))
75
80
  normalized_redirect_uri = normalize_redirect_uri_for_comparison(redirect_uri)
76
- redirect_authorize_error("redirect_uri") unless redirect_uris.include?(normalized_redirect_uri)
81
+ unless redirect_uris.include?(normalized_redirect_uri) || redirect_uris.include?(redirect_uri)
82
+ redirect_authorize_error("redirect_uri")
83
+ end
77
84
  elsif redirect_uris.size > 1
78
85
  redirect_authorize_error("redirect_uri")
79
86
  end
@@ -109,13 +116,20 @@ module Rodauth
109
116
  !approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
110
117
  end
111
118
 
119
+ def resource_owner_params
120
+ { oauth_grants_account_id_column => account_id }
121
+ end
122
+
123
+ def oauth_grants_resource_owner_columns
124
+ [oauth_grants_account_id_column]
125
+ end
126
+
112
127
  def try_approval_prompt
113
128
  approval_prompt = param_or_nil("approval_prompt")
114
129
 
115
130
  return unless approval_prompt && approval_prompt == "auto"
116
131
 
117
- return if db[oauth_grants_table].where(
118
- oauth_grants_account_id_column => account_id,
132
+ return if db[oauth_grants_table].where(resource_owner_params).where(
119
133
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
120
134
  oauth_grants_redirect_uri_column => redirect_uri,
121
135
  oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
@@ -193,9 +193,8 @@ module Rodauth
193
193
 
194
194
  # do not clean up device code just yet
195
195
  update_params.delete(oauth_grants_code_column)
196
-
197
196
  update_params[oauth_grants_user_code_column] = nil
198
- update_params[oauth_grants_account_id_column] = account_id
197
+ update_params.merge!(resource_params)
199
198
 
200
199
  super(grant_params, update_params)
201
200
  end
@@ -9,16 +9,78 @@ module Rodauth
9
9
  before "register"
10
10
 
11
11
  auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name]
12
+ auth_value_method :oauth_applications_registration_access_token_column, :registration_access_token
13
+ auth_value_method :registration_client_uri_route, "register"
12
14
 
13
15
  PROTECTED_APPLICATION_ATTRIBUTES = %w[account_id client_id].freeze
14
16
 
17
+ def load_registration_client_uri_routes
18
+ request.on(registration_client_uri_route) do
19
+ # CLIENT REGISTRATION URI
20
+ request.on(String) do |client_id|
21
+ (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Bearer (.*)\Z/, 1]))
22
+
23
+ next unless token
24
+
25
+ oauth_application = db[oauth_applications_table]
26
+ .where(oauth_applications_client_id_column => client_id)
27
+ .first
28
+ next unless oauth_application
29
+
30
+ authorization_required unless password_hash_match?(oauth_application[oauth_applications_registration_access_token_column], token)
31
+
32
+ request.is do
33
+ request.get do
34
+ json_response_oauth_application(oauth_application)
35
+ end
36
+ request.on method: :put do
37
+ %w[client_id registration_access_token registration_client_uri client_secret_expires_at
38
+ client_id_issued_at].each do |prohibited_param|
39
+ if request.params.key?(prohibited_param)
40
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(prohibited_param))
41
+ end
42
+ end
43
+ validate_client_registration_params
44
+
45
+ # if the client includes the "client_secret" field in the request, the value of this field MUST match the currently
46
+ # issued client secret for that client. The client MUST NOT be allowed to overwrite its existing client secret with
47
+ # its own chosen value.
48
+ authorization_required if request.params.key?("client_secret") && secret_matches?(oauth_application,
49
+ request.params["client_secret"])
50
+
51
+ oauth_application = transaction do
52
+ applications_ds = db[oauth_applications_table]
53
+ __update_and_return__(applications_ds, @oauth_application_params)
54
+ end
55
+ json_response_oauth_application(oauth_application)
56
+ end
57
+
58
+ request.on method: :delete do
59
+ applications_ds = db[oauth_applications_table]
60
+ applications_ds.where(oauth_applications_client_id_column => client_id).delete
61
+ response.status = 204
62
+ response["Cache-Control"] = "no-store"
63
+ response["Pragma"] = "no-cache"
64
+ response.finish
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
15
71
  # /register
16
72
  auth_server_route(:register) do |r|
17
73
  before_register_route
18
74
 
19
- validate_client_registration_params
20
-
21
75
  r.post do
76
+ oauth_client_registration_required_params.each do |required_param|
77
+ unless request.params.key?(required_param)
78
+ register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
79
+ end
80
+ end
81
+
82
+ validate_client_registration_params
83
+
22
84
  response_params = transaction do
23
85
  before_register
24
86
  do_register
@@ -57,12 +119,6 @@ module Rodauth
57
119
  end
58
120
 
59
121
  def validate_client_registration_params
60
- oauth_client_registration_required_params.each do |required_param|
61
- unless request.params.key?(required_param)
62
- register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
63
- end
64
- end
65
-
66
122
  @oauth_application_params = request.params.each_with_object({}) do |(key, value), params|
67
123
  case key
68
124
  when "redirect_uris"
@@ -96,7 +152,7 @@ module Rodauth
96
152
  key = oauth_applications_grant_types_column
97
153
  when "response_types"
98
154
  if value.is_a?(Array)
99
- grant_types = request.params["grant_types"] || oauth_grant_types_supported
155
+ grant_types = request.params["grant_types"] || %w[authorization_code]
100
156
  value = value.each do |response_type|
101
157
  unless oauth_response_types_supported.include?(response_type)
102
158
  register_throw_json_response_error("invalid_client_metadata",
@@ -114,7 +170,7 @@ module Rodauth
114
170
  register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value)) unless check_valid_uri?(value)
115
171
  case key
116
172
  when "client_uri"
117
- key = "homepage_url"
173
+ key = oauth_applications_homepage_url_column
118
174
  when "jwks_uri"
119
175
  if request.params.key?("jwks")
120
176
  register_throw_json_response_error("invalid_client_metadata",
@@ -132,6 +188,7 @@ module Rodauth
132
188
  key = oauth_applications_jwks_column
133
189
  value = JSON.dump(value)
134
190
  when "scope"
191
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(String)
135
192
  scopes = value.split(" ") - oauth_application_scopes
136
193
  register_throw_json_response_error("invalid_client_metadata", register_invalid_scopes_message(value)) unless scopes.empty?
137
194
  key = oauth_applications_scopes_column
@@ -141,7 +198,37 @@ module Rodauth
141
198
  value = value.join(" ")
142
199
  key = oauth_applications_contacts_column
143
200
  when "client_name"
201
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(String)
144
202
  key = oauth_applications_name_column
203
+ when "require_pushed_authorization_requests"
204
+ unless respond_to?(:oauth_applications_require_pushed_authorization_requests_column)
205
+ register_throw_json_response_error("invalid_client_metadata",
206
+ register_invalid_param_message(key))
207
+ end
208
+ request.params[key] = value = convert_to_boolean(key, value)
209
+
210
+ key = oauth_applications_require_pushed_authorization_requests_column
211
+ when "tls_client_certificate_bound_access_tokens"
212
+ property = :oauth_applications_tls_client_certificate_bound_access_tokens_column
213
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key)) unless respond_to?(property)
214
+
215
+ request.params[key] = value = convert_to_boolean(key, value)
216
+
217
+ key = oauth_applications_tls_client_certificate_bound_access_tokens_column
218
+ when /\Atls_client_auth_/
219
+ unless respond_to?(:"oauth_applications_#{key}_column")
220
+ register_throw_json_response_error("invalid_client_metadata",
221
+ register_invalid_param_message(key))
222
+ end
223
+
224
+ # client using the tls_client_auth authentication method MUST use exactly one of the below metadata
225
+ # parameters to indicate the certificate subject value that the authorization server is to expect when
226
+ # authenticating the respective client.
227
+ if params.any? { |k, _| k.to_s.start_with?("tls_client_auth_") }
228
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
229
+ end
230
+
231
+ key = __send__(:"oauth_applications_#{key}_column")
145
232
  else
146
233
  if respond_to?(:"oauth_applications_#{key}_column")
147
234
  if PROTECTED_APPLICATION_ATTRIBUTES.include?(key)
@@ -183,42 +270,60 @@ module Rodauth
183
270
 
184
271
  # set defaults
185
272
  create_params = @oauth_application_params
273
+
274
+ # If omitted, an authorization server MAY register a client with a default set of scopes
186
275
  create_params[oauth_applications_scopes_column] ||= return_params["scopes"] = oauth_application_scopes.join(" ")
276
+
277
+ # https://datatracker.ietf.org/doc/html/rfc7591#section-2
187
278
  if create_params[oauth_applications_grant_types_column] ||= begin
279
+ # If omitted, the default behavior is that the client will use only the "authorization_code" Grant Type.
188
280
  return_params["grant_types"] = %w[authorization_code] # rubocop:disable Lint/AssignmentInCondition
189
281
  "authorization_code"
190
282
  end
191
283
  create_params[oauth_applications_token_endpoint_auth_method_column] ||= begin
284
+ # If unspecified or omitted, the default is "client_secret_basic", denoting the HTTP Basic
285
+ # authentication scheme as specified in Section 2.3.1 of OAuth 2.0.
192
286
  return_params["token_endpoint_auth_method"] = "client_secret_basic"
193
287
  "client_secret_basic"
194
288
  end
195
289
  end
196
290
  create_params[oauth_applications_response_types_column] ||= begin
291
+ # If omitted, the default is that the client will use only the "code" response type.
197
292
  return_params["response_types"] = %w[code]
198
293
  "code"
199
294
  end
200
295
  rescue_from_uniqueness_error do
201
- client_id = oauth_unique_id_generator
202
- create_params[oauth_applications_client_id_column] = client_id
203
- return_params["client_id"] = client_id
204
- return_params["client_id_issued_at"] = Time.now.utc.iso8601
205
- if create_params.key?(oauth_applications_client_secret_column)
206
- set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
207
- return_params.delete("client_secret")
208
- else
209
- client_secret = oauth_unique_id_generator
210
- set_client_secret(create_params, client_secret)
211
- return_params["client_secret"] = client_secret
212
- return_params["client_secret_expires_at"] = 0
213
-
214
- create_params.delete_if { |k, _| !application_columns.include?(k) }
215
- end
296
+ initialize_register_params(create_params, return_params)
297
+ create_params.delete_if { |k, _| !application_columns.include?(k) }
216
298
  applications_ds.insert(create_params)
217
299
  end
218
300
 
219
301
  return_params
220
302
  end
221
303
 
304
+ def initialize_register_params(create_params, return_params)
305
+ client_id = oauth_unique_id_generator
306
+ create_params[oauth_applications_client_id_column] = client_id
307
+ return_params["client_id"] = client_id
308
+ return_params["client_id_issued_at"] = Time.now.utc.iso8601
309
+
310
+ registration_access_token = oauth_unique_id_generator
311
+ create_params[oauth_applications_registration_access_token_column] = secret_hash(registration_access_token)
312
+ return_params["registration_access_token"] = registration_access_token
313
+ return_params["registration_client_uri"] = "#{base_url}/#{registration_client_uri_route}/#{return_params['client_id']}"
314
+
315
+ if create_params.key?(oauth_applications_client_secret_column)
316
+ set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
317
+ return_params.delete("client_secret")
318
+ else
319
+ client_secret = oauth_unique_id_generator
320
+ set_client_secret(create_params, client_secret)
321
+ return_params["client_secret"] = client_secret
322
+ return_params["client_secret_expires_at"] = 0
323
+
324
+ end
325
+ end
326
+
222
327
  def register_throw_json_response_error(code, message)
223
328
  throw_json_response_error(oauth_invalid_response_status, code, message)
224
329
  end
@@ -264,6 +369,54 @@ module Rodauth
264
369
  "type '#{response_type}' to be allowed."
265
370
  end
266
371
 
372
+ def convert_to_boolean(key, value)
373
+ case value
374
+ when "true" then true
375
+ when "false" then false
376
+ else
377
+ register_throw_json_response_error(
378
+ "invalid_client_metadata",
379
+ register_invalid_param_message(key)
380
+ )
381
+ end
382
+ end
383
+
384
+ def json_response_oauth_application(oauth_application)
385
+ params = methods.map { |k| k.to_s[/\Aoauth_applications_(\w+)_column\z/, 1] }.compact
386
+
387
+ body = params.each_with_object({}) do |k, hash|
388
+ next if %w[id account_id client_id client_secret cliennt_secret_hash].include?(k)
389
+
390
+ value = oauth_application[__send__(:"oauth_applications_#{k}_column")]
391
+
392
+ next unless value
393
+
394
+ case k
395
+ when "redirect_uri"
396
+ hash["redirect_uris"] = value.split(" ")
397
+ when "token_endpoint_auth_method", "grant_types", "response_types", "request_uris", "post_logout_redirect_uris"
398
+ hash[k] = value.split(" ")
399
+ when "scopes"
400
+ hash["scope"] = value
401
+ when "jwks"
402
+ hash[k] = value.is_a?(String) ? JSON.parse(value) : value
403
+ when "homepage_url"
404
+ hash["client_uri"] = value
405
+ when "name"
406
+ hash["client_name"] = value
407
+ else
408
+ hash[k] = value
409
+ end
410
+ end
411
+
412
+ response.status = 200
413
+ response["Content-Type"] ||= json_response_content_type
414
+ response["Cache-Control"] = "no-store"
415
+ response["Pragma"] = "no-cache"
416
+ json_payload = _json_response_body(body)
417
+ return_response(json_payload)
418
+ end
419
+
267
420
  def oauth_server_metadata_body(*)
268
421
  super.tap do |data|
269
422
  data[:registration_endpoint] = register_url
@@ -42,7 +42,7 @@ module Rodauth
42
42
  oauth_grants_type_column => "implicit",
43
43
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
44
44
  oauth_grants_scopes_column => scopes,
45
- oauth_grants_account_id_column => account_id
45
+ **resource_owner_params
46
46
  }.merge(grant_params)
47
47
 
48
48
  generate_token(grant_params, false)
@@ -9,6 +9,8 @@ module Rodauth
9
9
 
10
10
  auth_value_method :oauth_jwt_access_tokens, true
11
11
 
12
+ auth_value_methods(:jwt_claims)
13
+
12
14
  def require_oauth_authorization(*scopes)
13
15
  return super unless oauth_jwt_access_tokens
14
16
 
@@ -24,7 +24,8 @@ module Rodauth
24
24
  :jwt_decode_no_key,
25
25
  :generate_jti,
26
26
  :oauth_jwt_issuer,
27
- :oauth_jwt_audience
27
+ :oauth_jwt_audience,
28
+ :resource_owner_params_from_jwt_claims
28
29
  )
29
30
 
30
31
  private
@@ -70,6 +71,10 @@ module Rodauth
70
71
  client_application[oauth_applications_client_id_column]
71
72
  end
72
73
 
74
+ def resource_owner_params_from_jwt_claims(claims)
75
+ { oauth_grants_account_id_column => claims["sub"] }
76
+ end
77
+
73
78
  def oauth_server_metadata_body(path = nil)
74
79
  metadata = super
75
80
  metadata.merge! \
@@ -81,14 +86,6 @@ module Rodauth
81
86
  @_jwt_key ||= (oauth_application_jwks(oauth_application) if oauth_application)
82
87
  end
83
88
 
84
- def _jwt_public_key
85
- @_jwt_public_key ||= if oauth_application
86
- oauth_application_jwks(oauth_application)
87
- else
88
- _jwt_key
89
- end
90
- end
91
-
92
89
  # Resource Server only!
93
90
  #
94
91
  # returns the jwks set from the authorization server.
@@ -152,10 +149,28 @@ module Rodauth
152
149
  A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
153
150
  ]
154
151
 
155
- def jwk_export(key)
152
+ def key_to_jwk(key)
156
153
  JSON::JWK.new(key)
157
154
  end
158
155
 
156
+ def jwk_export(key)
157
+ key_to_jwk(key)
158
+ end
159
+
160
+ def jwk_import(jwk)
161
+ JSON::JWK.new(jwk)
162
+ end
163
+
164
+ def jwk_key(jwk)
165
+ jwk = jwk_import(jwk) unless jwk.is_a?(JSON::JWK)
166
+ jwk.to_key
167
+ end
168
+
169
+ def jwk_thumbprint(jwk)
170
+ jwk = jwk_import(jwk) if jwk.is_a?(Hash)
171
+ jwk.thumbprint
172
+ end
173
+
159
174
  def jwt_encode(payload,
160
175
  jwks: nil,
161
176
  encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
@@ -287,8 +302,26 @@ module Rodauth
287
302
  auth_value_method :oauth_jwt_jwe_encryption_methods_supported, []
288
303
  end
289
304
 
305
+ def key_to_jwk(key)
306
+ JWT::JWK.new(key)
307
+ end
308
+
290
309
  def jwk_export(key)
291
- JWT::JWK.new(key).export
310
+ key_to_jwk(key).export
311
+ end
312
+
313
+ def jwk_import(jwk)
314
+ JWT::JWK.import(jwk)
315
+ end
316
+
317
+ def jwk_key(jwk)
318
+ jwk = jwk_import(jwk) unless jwk.is_a?(JWT::JWK)
319
+ jwk.keypair
320
+ end
321
+
322
+ def jwk_thumbprint(jwk)
323
+ jwk = jwk_import(jwk) if jwk.is_a?(Hash)
324
+ JWT::JWK::Thumbprint.new(jwk).generate
292
325
  end
293
326
 
294
327
  def jwt_encode(payload,
@@ -445,6 +478,14 @@ module Rodauth
445
478
  raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
446
479
  end
447
480
 
481
+ def jwk_import(_jwk)
482
+ raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
483
+ end
484
+
485
+ def jwk_thumbprint(_jwk)
486
+ raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
487
+ end
488
+
448
489
  def jwt_encode(_token)
449
490
  raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
450
491
  end
@@ -46,28 +46,7 @@ module Rodauth
46
46
  request_object = response.body
47
47
  end
48
48
 
49
- request_sig_enc_opts = {
50
- jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
51
- jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
52
- jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
53
- }.compact
54
-
55
- request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
56
-
57
- if request_sig_enc_opts[:jws_algorithm] == "none"
58
- jwks = nil
59
- elsif (jwks = oauth_application_jwks(oauth_application))
60
- jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
61
- else
62
- redirect_response_error("invalid_request_object")
63
- end
64
-
65
- claims = jwt_decode(request_object,
66
- jwks: jwks,
67
- verify_jti: false,
68
- verify_iss: false,
69
- verify_aud: false,
70
- **request_sig_enc_opts)
49
+ claims = decode_request_object(request_object)
71
50
 
72
51
  redirect_response_error("invalid_request_object") unless claims
73
52
 
@@ -105,6 +84,35 @@ module Rodauth
105
84
  request_uris.nil? || request_uris.split(oauth_scope_separator).one? { |uri| request_uri.start_with?(uri) }
106
85
  end
107
86
 
87
+ def decode_request_object(request_object)
88
+ request_sig_enc_opts = {
89
+ jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
90
+ jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
91
+ jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
92
+ }.compact
93
+
94
+ request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
95
+
96
+ if request_sig_enc_opts[:jws_algorithm] == "none"
97
+ jwks = nil
98
+ elsif (jwks = oauth_application_jwks(oauth_application))
99
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
100
+ else
101
+ redirect_response_error("invalid_request_object")
102
+ end
103
+
104
+ claims = jwt_decode(request_object,
105
+ jwks: jwks,
106
+ verify_jti: false,
107
+ verify_iss: false,
108
+ verify_aud: false,
109
+ **request_sig_enc_opts)
110
+
111
+ redirect_response_error("invalid_request_object") unless claims
112
+
113
+ claims
114
+ end
115
+
108
116
  def oauth_server_metadata_body(*)
109
117
  super.tap do |data|
110
118
  data[:request_parameter_supported] = true
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth"
4
+
5
+ module Rodauth
6
+ Feature.define(:oauth_pushed_authorization_request, :OauthJwtPushedAuthorizationRequest) do
7
+ depends :oauth_authorize_base
8
+
9
+ auth_value_method :oauth_require_pushed_authorization_requests, false
10
+ auth_value_method :oauth_applications_require_pushed_authorization_requests_column, :require_pushed_authorization_requests
11
+ auth_value_method :oauth_pushed_authorization_request_expires_in, 90 # 90 seconds
12
+ auth_value_method :oauth_require_pushed_authorization_request_iss_request_object, true
13
+
14
+ auth_value_method :oauth_pushed_authorization_requests_table, :oauth_pushed_requests
15
+
16
+ %i[
17
+ oauth_application_id params code expires_in
18
+ ].each do |column|
19
+ auth_value_method :"oauth_pushed_authorization_requests_#{column}_column", column
20
+ end
21
+
22
+ # /par
23
+ auth_server_route(:par) do |r|
24
+ require_oauth_application
25
+ before_par_route
26
+
27
+ r.post do
28
+ validate_par_params
29
+
30
+ ds = db[oauth_pushed_authorization_requests_table]
31
+
32
+ code = oauth_unique_id_generator
33
+ push_request_params = {
34
+ oauth_pushed_authorization_requests_oauth_application_id_column => oauth_application[oauth_applications_id_column],
35
+ oauth_pushed_authorization_requests_code_column => code,
36
+ oauth_pushed_authorization_requests_params_column => URI.encode_www_form(request.params),
37
+ oauth_pushed_authorization_requests_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
38
+ seconds: oauth_pushed_authorization_request_expires_in)
39
+ }
40
+
41
+ rescue_from_uniqueness_error do
42
+ ds.insert(push_request_params)
43
+ end
44
+
45
+ json_response_success(
46
+ "request_uri" => "urn:ietf:params:oauth:request_uri:#{code}",
47
+ "expires_in" => oauth_pushed_authorization_request_expires_in
48
+ )
49
+ end
50
+ end
51
+
52
+ def check_csrf?
53
+ case request.path
54
+ when par_path
55
+ false
56
+ else
57
+ super
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def validate_par_params
64
+ # https://datatracker.ietf.org/doc/html/rfc9126#section-2.1
65
+ # The request_uri authorization request parameter is one exception, and it MUST NOT be provided.
66
+ redirect_response_error("invalid_request") if param_or_nil("request_uri")
67
+
68
+ if (request_object = param_or_nil("request")) && features.include?(:oauth_jwt_secured_authorization_request)
69
+ claims = decode_request_object(request_object)
70
+
71
+ # https://datatracker.ietf.org/doc/html/rfc9126#section-3-5.3
72
+ # reject the request if the authenticated client_id does not match the client_id claim in the Request Object
73
+ if (client_id = claims["client_id"]) && (client_id != oauth_application[oauth_applications_client_id_column])
74
+ redirect_response_error("invalid_request_object")
75
+ end
76
+
77
+ # requiring the iss claim to match the client_id is at the discretion of the authorization server
78
+ if oauth_require_pushed_authorization_request_iss_request_object &&
79
+ (iss = claims.delete("iss")) &&
80
+ iss != oauth_application[oauth_applications_client_id_column]
81
+ redirect_response_error("invalid_request_object")
82
+ end
83
+
84
+ if (aud = claims.delete("aud")) && !verify_aud(aud, oauth_jwt_issuer)
85
+ redirect_response_error("invalid_request_object")
86
+ end
87
+
88
+ claims.delete("exp")
89
+ request.params.delete("request")
90
+
91
+ claims.each do |k, v|
92
+ request.params[k.to_s] = v
93
+ end
94
+ end
95
+
96
+ validate_authorize_params
97
+ end
98
+
99
+ def validate_authorize_params
100
+ return super unless request.get? && request.path == authorize_path
101
+
102
+ if (request_uri = param_or_nil("request_uri"))
103
+ code = request_uri.delete_prefix("urn:ietf:params:oauth:request_uri:")
104
+
105
+ table = oauth_pushed_authorization_requests_table
106
+ ds = db[table]
107
+
108
+ pushed_request = ds.where(
109
+ oauth_pushed_authorization_requests_oauth_application_id_column => oauth_application[oauth_applications_id_column],
110
+ oauth_pushed_authorization_requests_code_column => code
111
+ ).where(
112
+ Sequel.expr(Sequel[table][oauth_pushed_authorization_requests_expires_in_column]) >= Sequel::CURRENT_TIMESTAMP
113
+ ).first
114
+
115
+ redirect_response_error("invalid_request") unless pushed_request
116
+
117
+ URI.decode_www_form(pushed_request[oauth_pushed_authorization_requests_params_column]).each do |k, v|
118
+ request.params[k.to_s] = v
119
+ end
120
+
121
+ elsif oauth_require_pushed_authorization_requests ||
122
+ (oauth_application && oauth_application[oauth_applications_require_pushed_authorization_requests_column])
123
+ redirect_authorize_error("request_uri")
124
+ end
125
+ super
126
+ end
127
+
128
+ def oauth_server_metadata_body(*)
129
+ super.tap do |data|
130
+ data[:require_pushed_authorization_requests] = oauth_require_pushed_authorization_requests
131
+ data[:pushed_authorization_request_endpoint] = par_url
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "ipaddr"
5
+ require "uri"
6
+ require "rodauth/oauth"
7
+
8
+ module Rodauth
9
+ Feature.define(:oauth_tls_client_auth, :OauthTlsClientAuth) do
10
+ depends :oauth_jwt_base
11
+
12
+ auth_value_method :oauth_tls_client_certificate_bound_access_tokens, false
13
+
14
+ %i[
15
+ tls_client_auth_subject_dn tls_client_auth_san_dns
16
+ tls_client_auth_san_uri tls_client_auth_san_ip
17
+ tls_client_auth_san_email tls_client_certificate_bound_access_tokens
18
+ ].each do |column|
19
+ auth_value_method :"oauth_applications_#{column}_column", column
20
+ end
21
+
22
+ auth_value_method :oauth_grants_certificate_thumbprint_column, :certificate_thumbprint
23
+
24
+ def oauth_token_endpoint_auth_methods_supported
25
+ super | %w[tls_client_auth self_signed_tls_client_auth]
26
+ end
27
+
28
+ private
29
+
30
+ def validate_token_params
31
+ # For all requests to the authorization server utilizing mutual-TLS client authentication,
32
+ # the client MUST include the client_id parameter
33
+ redirect_response_error("invalid_request") if client_certificate && !param_or_nil("client_id")
34
+
35
+ super
36
+ end
37
+
38
+ def require_oauth_application
39
+ return super unless client_certificate
40
+
41
+ authorization_required unless oauth_application
42
+
43
+ if supports_auth_method?(oauth_application, "tls_client_auth")
44
+ # It relies on a validated certificate chain [RFC5280]
45
+
46
+ ssl_verify = request.env["SSL_CLIENT_VERIFY"] || request.env["HTTP_SSL_CLIENT_VERIFY"] || request.env["HTTP_X_SSL_CLIENT_VERIFY"]
47
+
48
+ authorization_required unless ssl_verify == "SUCCESS"
49
+
50
+ # and a single subject distinguished name (DN) or a single subject alternative name (SAN) to
51
+ # authenticate the client. Only one subject name value of any type is used for each client.
52
+
53
+ name_matches = if oauth_application[:tls_client_auth_subject_dn]
54
+ distinguished_name_match?(client_certificate.subject, oauth_application[:tls_client_auth_subject_dn])
55
+ elsif (dns = oauth_application[:tls_client_auth_san_dns])
56
+ client_certificate_sans.any? { |san| san.tag == 2 && OpenSSL::SSL.verify_hostname(dns, san.value) }
57
+ elsif (uri = oauth_application[:tls_client_auth_san_uri])
58
+ uri = URI(uri)
59
+ client_certificate_sans.any? { |san| san.tag == 6 && URI(san.value) == uri }
60
+ elsif (ip = oauth_application[:tls_client_auth_san_ip])
61
+ ip = IPAddr.new(ip).hton
62
+ client_certificate_sans.any? { |san| san.tag == 7 && san.value == ip }
63
+ elsif (email = oauth_application[:tls_client_auth_san_email])
64
+ client_certificate_sans.any? { |san| san.tag == 1 && san.value == email }
65
+ else
66
+ false
67
+ end
68
+ authorization_required unless name_matches
69
+
70
+ oauth_application
71
+ elsif supports_auth_method?(oauth_application, "self_signed_tls_client_auth")
72
+ jwks = oauth_application_jwks(oauth_application)
73
+
74
+ thumbprint = jwk_thumbprint(key_to_jwk(client_certificate.public_key))
75
+
76
+ # The client is successfully authenticated if the certificate that it presented during the handshake
77
+ # matches one of the certificates configured or registered for that particular client.
78
+ authorization_required unless jwks.any? { |jwk| Array(jwk[:x5c]).first == thumbprint }
79
+
80
+ oauth_application
81
+ else
82
+ super
83
+ end
84
+ rescue URI::InvalidURIError, IPAddr::InvalidAddressError
85
+ authorization_required
86
+ end
87
+
88
+ def store_token(grant_params, update_params = {})
89
+ return super unless client_certificate && (
90
+ oauth_tls_client_certificate_bound_access_tokens ||
91
+ oauth_application[oauth_applications_tls_client_certificate_bound_access_tokens_column]
92
+ )
93
+
94
+ update_params[oauth_grants_certificate_thumbprint_column] = jwk_thumbprint(key_to_jwk(client_certificate.public_key))
95
+ super
96
+ end
97
+
98
+ def jwt_claims(oauth_grant)
99
+ claims = super
100
+
101
+ return claims unless oauth_grant[oauth_grants_certificate_thumbprint_column]
102
+
103
+ claims[:cnf] = {
104
+ "x5t#S256" => oauth_grant[oauth_grants_certificate_thumbprint_column]
105
+ }
106
+
107
+ claims
108
+ end
109
+
110
+ def json_token_introspect_payload(grant_or_claims)
111
+ claims = super
112
+
113
+ return claims unless grant_or_claims && grant_or_claims[oauth_grants_certificate_thumbprint_column]
114
+
115
+ claims[:cnf] = {
116
+ "x5t#S256" => grant_or_claims[oauth_grants_certificate_thumbprint_column]
117
+ }
118
+
119
+ claims
120
+ end
121
+
122
+ def oauth_server_metadata_body(*)
123
+ super.tap do |data|
124
+ data[:tls_client_certificate_bound_access_tokens] = oauth_tls_client_certificate_bound_access_tokens
125
+ end
126
+ end
127
+
128
+ def client_certificate
129
+ return @client_certificate if defined?(@client_certificate)
130
+
131
+ unless (pem_cert = request.env["SSL_CLIENT_CERT"] || request.env["HTTP_SSL_CLIENT_CERT"] || request.env["HTTP_X_SSL_CLIENT_CERT"])
132
+ return
133
+ end
134
+
135
+ return if pem_cert.empty?
136
+
137
+ @certificate = OpenSSL::X509::Certificate.new(pem_cert)
138
+ end
139
+
140
+ def client_certificate_sans
141
+ return @client_certificate_sans if defined?(@client_certificate_sans)
142
+
143
+ @client_certificate_sans = begin
144
+ return [] unless client_certificate
145
+
146
+ san = client_certificate.extensions.find { |ext| ext.oid == "subjectAltName" }
147
+
148
+ return [] unless san
149
+
150
+ ostr = OpenSSL::ASN1.decode(san.to_der).value.last
151
+
152
+ sans = OpenSSL::ASN1.decode(ostr.value)
153
+
154
+ return [] unless sans
155
+
156
+ sans.value
157
+ end
158
+ end
159
+
160
+ def distinguished_name_match?(sub1, sub2)
161
+ sub1 = OpenSSL::X509::Name.parse(sub1) if sub1.is_a?(String)
162
+ sub2 = OpenSSL::X509::Name.parse(sub2) if sub2.is_a?(String)
163
+ # OpenSSL::X509::Name#cp calls X509_NAME_cmp via openssl.
164
+ # https://www.openssl.org/docs/manmaster/man3/X509_NAME_cmp.html
165
+ # This procedure adheres to the matching rules for Distinguished Names (DN) given in
166
+ # RFC 4517 section 4.2.15 and RFC 5280 section 7.1.
167
+ sub1.cmp(sub2).zero?
168
+ end
169
+ end
170
+ end
@@ -68,7 +68,7 @@ module Rodauth
68
68
  auth_value_method :oauth_application_scopes, %w[openid]
69
69
 
70
70
  %i[
71
- subject_type application_type sector_identifier_uri
71
+ subject_type application_type sector_identifier_uri initiate_login_uri
72
72
  id_token_signed_response_alg id_token_encrypted_response_alg id_token_encrypted_response_enc
73
73
  userinfo_signed_response_alg userinfo_encrypted_response_alg userinfo_encrypted_response_enc
74
74
  ].each do |column|
@@ -112,7 +112,7 @@ module Rodauth
112
112
 
113
113
  throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
114
114
 
115
- account = db[accounts_table].where(account_id_column => claims["sub"]).first
115
+ account = account_ds(claims["sub"]).first
116
116
 
117
117
  throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless account
118
118
 
@@ -126,7 +126,7 @@ module Rodauth
126
126
 
127
127
  oauth_grant = valid_oauth_grant_ds(
128
128
  oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
129
- oauth_grants_account_id_column => account[account_id_column]
129
+ **resource_owner_params_from_jwt_claims(claims)
130
130
  ).first
131
131
 
132
132
  claims_locales = oauth_grant[oauth_grants_claims_locales_column] if oauth_grant
@@ -333,8 +333,9 @@ module Rodauth
333
333
 
334
334
  identifier_uri = URI(identifier_uri).host
335
335
 
336
- account_id = oauth_grant[oauth_grants_account_id_column]
337
- Digest::SHA256.hexdigest("#{identifier_uri}#{account_id}#{oauth_jwt_subject_secret}")
336
+ account_ids = oauth_grant.values_at(oauth_grants_resource_owner_columns)
337
+ values = [identifier_uri, *account_ids, oauth_jwt_subject_secret]
338
+ Digest::SHA256.hexdigest(values.join)
338
339
  else
339
340
  raise StandardError, "unexpected subject (#{subject_type})"
340
341
  end
@@ -434,8 +435,8 @@ module Rodauth
434
435
  end
435
436
 
436
437
  def create_oauth_grant_with_token(create_params = {})
438
+ create_params.merge!(resource_owner_params)
437
439
  create_params[oauth_grants_type_column] = "hybrid"
438
- create_params[oauth_grants_account_id_column] = account_id
439
440
  create_params[oauth_grants_expires_in_column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in)
440
441
  authorization_code = create_oauth_grant(create_params)
441
442
  access_token = if oauth_jwt_access_tokens
@@ -587,14 +588,14 @@ module Rodauth
587
588
  additional_info = additional_claims_info[param] || EMPTY_HASH
588
589
  value = additional_info["value"] || meth[account, param]
589
590
  value = nil if additional_info["values"] && additional_info["values"].include?(value)
590
- cl[param] = value if value
591
+ cl[param] = value unless value.nil?
591
592
  end
592
593
  elsif claims_locales.nil?
593
594
  lambda do |account, param, cl = claims|
594
595
  additional_info = additional_claims_info[param] || EMPTY_HASH
595
596
  value = additional_info["value"] || meth[account, param, nil]
596
597
  value = nil if additional_info["values"] && additional_info["values"].include?(value)
597
- cl[param] = value if value
598
+ cl[param] = value unless value.nil?
598
599
  end
599
600
  else
600
601
  lambda do |account, param, cl = claims|
@@ -691,7 +692,7 @@ module Rodauth
691
692
 
692
693
  def oidc_grant_params
693
694
  grant_params = {
694
- oauth_grants_account_id_column => account_id,
695
+ **resource_owner_params,
695
696
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
696
697
  oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
697
698
  }
@@ -43,7 +43,11 @@ module Rodauth
43
43
  end
44
44
  end
45
45
 
46
- if (value = @oauth_application_params[oauth_applications_sector_identifier_uri_column])
46
+ if (value = @oauth_application_params[oauth_applications_sector_identifier_uri_column]) && !check_valid_uri?(value)
47
+ register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
48
+ end
49
+
50
+ if (value = @oauth_application_params[oauth_applications_initiate_login_uri_column])
47
51
  uri = URI(value)
48
52
 
49
53
  unless uri.scheme == "https" || uri.host == "localhost"
@@ -219,5 +223,13 @@ module Rodauth
219
223
  def register_invalid_application_type_message(application_type)
220
224
  "The application type '#{application_type}' is not allowed."
221
225
  end
226
+
227
+ def initialize_register_params(create_params, return_params)
228
+ super
229
+ registration_access_token = oauth_unique_id_generator
230
+ create_params[oauth_applications_registration_access_token_column] = secret_hash(registration_access_token)
231
+ return_params["registration_access_token"] = registration_access_token
232
+ return_params["registration_client_uri"] = "#{base_url}/#{registration_client_uri_route}/#{return_params['client_id']}"
233
+ end
222
234
  end
223
235
  end
@@ -36,10 +36,9 @@ module Rodauth
36
36
 
37
37
  oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["aud"]).first
38
38
  oauth_grant = db[oauth_grants_table]
39
- .where(
40
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
41
- oauth_grants_account_id_column => account_id
42
- ).first
39
+ .where(resource_owner_params)
40
+ .where(oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column])
41
+ .first
43
42
 
44
43
  # check whether ID token belongs to currently logged-in user
45
44
  redirect_logout_with_error(oauth_invalid_client_message) unless oauth_grant && claims["sub"] == jwt_subject(oauth_grant,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "1.1.0"
5
+ VERSION = "1.2.0"
6
6
  end
7
7
  end
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: 1.1.0
4
+ version: 1.2.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: 2023-01-10 00:00:00.000000000 Z
11
+ date: 2023-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rodauth
@@ -68,6 +68,7 @@ extra_rdoc_files:
68
68
  - doc/release_notes/0_9_3.md
69
69
  - doc/release_notes/1_0_0.md
70
70
  - doc/release_notes/1_1_0.md
71
+ - doc/release_notes/1_2_0.md
71
72
  files:
72
73
  - CHANGELOG.md
73
74
  - LICENSE.txt
@@ -107,6 +108,7 @@ files:
107
108
  - doc/release_notes/0_9_3.md
108
109
  - doc/release_notes/1_0_0.md
109
110
  - doc/release_notes/1_1_0.md
111
+ - doc/release_notes/1_2_0.md
110
112
  - lib/generators/rodauth/oauth/install_generator.rb
111
113
  - lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
112
114
  - lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
@@ -138,9 +140,11 @@ files:
138
140
  - lib/rodauth/features/oauth_jwt_secured_authorization_request.rb
139
141
  - lib/rodauth/features/oauth_management_base.rb
140
142
  - lib/rodauth/features/oauth_pkce.rb
143
+ - lib/rodauth/features/oauth_pushed_authorization_request.rb
141
144
  - lib/rodauth/features/oauth_resource_indicators.rb
142
145
  - lib/rodauth/features/oauth_resource_server.rb
143
146
  - lib/rodauth/features/oauth_saml_bearer_grant.rb
147
+ - lib/rodauth/features/oauth_tls_client_auth.rb
144
148
  - lib/rodauth/features/oauth_token_introspection.rb
145
149
  - lib/rodauth/features/oauth_token_revocation.rb
146
150
  - lib/rodauth/features/oidc.rb