rodauth-oauth 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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