rodauth-oauth 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -56,14 +118,8 @@ module Rodauth
56
118
  }
57
119
  end
58
120
 
59
- 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
- @oauth_application_params = request.params.each_with_object({}) do |(key, value), params|
121
+ def validate_client_registration_params(request_params = request.params)
122
+ @oauth_application_params = request_params.each_with_object({}) do |(key, value), params|
67
123
  case key
68
124
  when "redirect_uris"
69
125
  if value.is_a?(Array)
@@ -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,9 +170,9 @@ 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
- if request.params.key?("jwks")
175
+ if request_params.key?("jwks")
120
176
  register_throw_json_response_error("invalid_client_metadata",
121
177
  register_invalid_jwks_param_message(key, "jwks"))
122
178
  end
@@ -124,7 +180,7 @@ module Rodauth
124
180
  key = __send__(:"oauth_applications_#{key}_column")
125
181
  when "jwks"
126
182
  register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
127
- if request.params.key?("jwks_uri")
183
+ if request_params.key?("jwks_uri")
128
184
  register_throw_json_response_error("invalid_client_metadata",
129
185
  register_invalid_jwks_param_message(key, "jwks_uri"))
130
186
  end
@@ -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
@@ -20,6 +20,24 @@ module Rodauth
20
20
 
21
21
  private
22
22
 
23
+ def validate_authorize_params
24
+ super
25
+
26
+ response_mode = param_or_nil("response_mode")
27
+
28
+ return unless response_mode
29
+
30
+ response_type = param_or_nil("response_type")
31
+
32
+ return unless response_type == "token"
33
+
34
+ redirect_response_error("invalid_request") unless oauth_response_modes_for_token_supported.include?(response_mode)
35
+ end
36
+
37
+ def oauth_response_modes_for_token_supported
38
+ %w[fragment]
39
+ end
40
+
23
41
  def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
24
42
  response_type = param("response_type")
25
43
  return super unless response_type == "token" && supported_response_type?(response_type)
@@ -42,19 +60,19 @@ module Rodauth
42
60
  oauth_grants_type_column => "implicit",
43
61
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
44
62
  oauth_grants_scopes_column => scopes,
45
- oauth_grants_account_id_column => account_id
63
+ **resource_owner_params
46
64
  }.merge(grant_params)
47
65
 
48
66
  generate_token(grant_params, false)
49
67
  end
50
68
 
51
- def _redirect_response_error(redirect_url, query_params)
69
+ def _redirect_response_error(redirect_url, params)
52
70
  response_types = param("response_type").split(/ +/)
53
71
 
54
72
  return super if response_types.empty? || response_types == %w[code]
55
73
 
56
- query_params = query_params.map { |k, v| "#{k}=#{v}" }
57
- redirect_url.fragment = query_params.join("&")
74
+ params = params.map { |k, v| "#{k}=#{v}" }
75
+ redirect_url.fragment = params.join("&")
58
76
  redirect(redirect_url.to_s)
59
77
  end
60
78
 
@@ -62,7 +80,7 @@ module Rodauth
62
80
  return super unless mode == "fragment"
63
81
 
64
82
  redirect_url = URI.parse(redirect_uri)
65
- params = params.map { |k, v| "#{k}=#{v}" }
83
+ params = [URI.encode_www_form(params)]
66
84
  params << redirect_url.query if redirect_url.query
67
85
  redirect_url.fragment = params.join("&")
68
86
  redirect(redirect_url.to_s)
@@ -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,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth"
4
+
5
+ module Rodauth
6
+ Feature.define(:oauth_jwt_secured_authorization_response_mode, :OauthJwtSecuredAuthorizationResponseMode) do
7
+ depends :oauth_authorize_base, :oauth_jwt_base
8
+
9
+ auth_value_method :oauth_authorization_response_mode_expires_in, 60 * 5 # 5 minutes
10
+
11
+ auth_value_method :oauth_applications_authorization_signed_response_alg_column, :authorization_signed_response_alg
12
+ auth_value_method :oauth_applications_authorization_encrypted_response_alg_column, :authorization_encrypted_response_alg
13
+ auth_value_method :oauth_applications_authorization_encrypted_response_enc_column, :authorization_encrypted_response_enc
14
+
15
+ auth_value_methods(
16
+ :authorization_signing_alg_values_supported,
17
+ :authorization_encryption_alg_values_supported,
18
+ :authorization_encryption_enc_values_supported
19
+ )
20
+
21
+ def oauth_response_modes_supported
22
+ jwt_response_modes = %w[jwt]
23
+ jwt_response_modes.push("query.jwt", "form_post.jwt") if features.include?(:oauth_authorization_code_grant)
24
+ jwt_response_modes << "fragment.jwt" if features.include?(:oauth_implicit_grant)
25
+
26
+ super | jwt_response_modes
27
+ end
28
+
29
+ def authorization_signing_alg_values_supported
30
+ oauth_jwt_jws_algorithms_supported
31
+ end
32
+
33
+ def authorization_encryption_alg_values_supported
34
+ oauth_jwt_jwe_algorithms_supported
35
+ end
36
+
37
+ def authorization_encryption_enc_values_supported
38
+ oauth_jwt_jwe_encryption_methods_supported
39
+ end
40
+
41
+ private
42
+
43
+ def oauth_response_modes_for_code_supported
44
+ return [] unless features.include?(:oauth_authorization_code_grant)
45
+
46
+ super | %w[query.jwt form_post.jwt jwt]
47
+ end
48
+
49
+ def oauth_response_modes_for_token_supported
50
+ return [] unless features.include?(:oauth_implicit_grant)
51
+
52
+ super | %w[fragment.jwt jwt]
53
+ end
54
+
55
+ def authorize_response(params, mode)
56
+ return super unless mode.end_with?("jwt")
57
+
58
+ response_type = param_or_nil("response_type")
59
+
60
+ redirect_url = URI.parse(redirect_uri)
61
+
62
+ jwt = jwt_encode_authorization_response_mode(params)
63
+
64
+ if mode == "query.jwt" || (mode == "jwt" && response_type == "code")
65
+ return super unless features.include?(:oauth_authorization_code_grant)
66
+
67
+ params = ["response=#{CGI.escape(jwt)}"]
68
+ params << redirect_url.query if redirect_url.query
69
+ redirect_url.query = params.join("&")
70
+ redirect(redirect_url.to_s)
71
+ elsif mode == "form_post.jwt"
72
+ return super unless features.include?(:oauth_authorization_code_grant)
73
+
74
+ response["Content-Type"] = "text/html"
75
+ body = form_post_response_html(redirect_url) do
76
+ "<input type=\"hidden\" name=\"response\" value=\"#{scope.h(jwt)}\" />"
77
+ end
78
+ response.write(body)
79
+ request.halt
80
+ elsif mode == "fragment.jwt" || (mode == "jwt" && response_type == "token")
81
+ return super unless features.include?(:oauth_implicit_grant)
82
+
83
+ params = ["response=#{CGI.escape(jwt)}"]
84
+ params << redirect_url.query if redirect_url.query
85
+ redirect_url.fragment = params.join("&")
86
+ redirect(redirect_url.to_s)
87
+ else
88
+ super
89
+ end
90
+ end
91
+
92
+ def _redirect_response_error(redirect_url, params)
93
+ response_mode = param_or_nil("response_mode")
94
+ return super unless response_mode.end_with?("jwt")
95
+
96
+ authorize_response(Hash[params], response_mode)
97
+ end
98
+
99
+ def jwt_encode_authorization_response_mode(params)
100
+ now = Time.now.to_i
101
+ claims = {
102
+ iss: oauth_jwt_issuer,
103
+ aud: oauth_application[oauth_applications_client_id_column],
104
+ exp: now + oauth_authorization_response_mode_expires_in,
105
+ iat: now
106
+ }.merge(params)
107
+
108
+ encode_params = {
109
+ jwks: oauth_application_jwks(oauth_application),
110
+ signing_algorithm: oauth_application[oauth_applications_authorization_signed_response_alg_column],
111
+ encryption_algorithm: oauth_application[oauth_applications_authorization_encrypted_response_alg_column],
112
+ encryption_method: oauth_application[oauth_applications_authorization_encrypted_response_enc_column]
113
+ }.compact
114
+
115
+ jwt_encode(claims, **encode_params)
116
+ end
117
+
118
+ def oauth_server_metadata_body(*)
119
+ super.tap do |data|
120
+ data[:authorization_signing_alg_values_supported] = authorization_signing_alg_values_supported
121
+ data[:authorization_encryption_alg_values_supported] = authorization_encryption_alg_values_supported
122
+ data[:authorization_encryption_enc_values_supported] = authorization_encryption_enc_values_supported
123
+ end
124
+ end
125
+ end
126
+ end
@@ -23,9 +23,7 @@ module Rodauth
23
23
  classes += " disabled" if current || !page
24
24
  classes += " active" if current
25
25
  if page
26
- params = request.GET.merge("page" => page).map do |k, v|
27
- v ? "#{CGI.escape(String(k))}=#{CGI.escape(String(v))}" : CGI.escape(String(k))
28
- end.join("&")
26
+ params = URI.encode_www_form(request.GET.merge("page" => page))
29
27
 
30
28
  href = "#{request.path}?#{params}"
31
29