rodauth-oauth 0.8.0 → 0.9.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -2
  3. data/doc/release_notes/0_9_0.md +56 -0
  4. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +22 -1
  5. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +8 -3
  6. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +8 -2
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +1 -0
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +1 -0
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +1 -0
  10. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +13 -1
  11. data/lib/rodauth/features/oauth.rb +2 -2
  12. data/lib/rodauth/features/oauth_application_management.rb +22 -6
  13. data/lib/rodauth/features/oauth_assertion_base.rb +1 -1
  14. data/lib/rodauth/features/oauth_authorization_code_grant.rb +4 -1
  15. data/lib/rodauth/features/oauth_base.rb +46 -10
  16. data/lib/rodauth/features/oauth_client_credentials_grant.rb +33 -0
  17. data/lib/rodauth/features/oauth_device_grant.rb +4 -5
  18. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +252 -0
  19. data/lib/rodauth/features/oauth_jwt.rb +248 -49
  20. data/lib/rodauth/features/oauth_management_base.rb +68 -0
  21. data/lib/rodauth/features/oauth_pkce.rb +1 -1
  22. data/lib/rodauth/features/oauth_token_management.rb +8 -6
  23. data/lib/rodauth/features/oidc.rb +32 -3
  24. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +147 -0
  25. data/lib/rodauth/oauth/jwe_extensions.rb +64 -0
  26. data/lib/rodauth/oauth/ttl_store.rb +9 -3
  27. data/lib/rodauth/oauth/version.rb +1 -1
  28. data/locales/en.yml +5 -0
  29. data/templates/authorize.str +50 -1
  30. data/templates/jwks_field.str +4 -0
  31. data/templates/oauth_application.str +1 -1
  32. data/templates/oauth_application_oauth_tokens.str +1 -0
  33. data/templates/oauth_applications.str +1 -0
  34. data/templates/oauth_tokens.str +1 -0
  35. metadata +10 -3
  36. data/templates/jws_jwk_field.str +0 -4
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:oauth_dynamic_client_registration, :OauthDynamicClientRegistration) do
5
+ depends :oauth_base
6
+
7
+ before "register"
8
+
9
+ auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name client_uri]
10
+
11
+ PROTECTED_APPLICATION_ATTRIBUTES = %i[account_id client_id].freeze
12
+
13
+ # /register
14
+ route(:register) do |r|
15
+ next unless is_authorization_server?
16
+
17
+ before_register_route
18
+
19
+ validate_client_registration_params
20
+
21
+ r.post do
22
+ response_params = transaction do
23
+ before_register
24
+ do_register
25
+ end
26
+
27
+ response.status = 201
28
+ response["Content-Type"] = json_response_content_type
29
+ response["Cache-Control"] = "no-store"
30
+ response["Pragma"] = "no-cache"
31
+ response.write(_json_response_body(response_params))
32
+ end
33
+ end
34
+
35
+ def check_csrf?
36
+ case request.path
37
+ when register_path
38
+ false
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def registration_metadata
47
+ oauth_server_metadata_body
48
+ end
49
+
50
+ def validate_client_registration_params
51
+ oauth_client_registration_required_params.each do |required_param|
52
+ unless request.params.key?(required_param)
53
+ register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
54
+ end
55
+ end
56
+ metadata = registration_metadata
57
+
58
+ @oauth_application_params = request.params.each_with_object({}) do |(key, value), params|
59
+ case key
60
+ when "redirect_uris"
61
+ if value.is_a?(Array)
62
+ value = value.each do |uri|
63
+ register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(uri)) unless check_valid_uri?(uri)
64
+ end.join(" ")
65
+ else
66
+ register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
67
+ end
68
+ key = oauth_applications_redirect_uri_column
69
+ when "token_endpoint_auth_method"
70
+ unless oauth_auth_methods_supported.include?(value)
71
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
72
+ end
73
+ # verify if in range
74
+ key = oauth_applications_token_endpoint_auth_method_column
75
+ when "grant_types"
76
+ if value.is_a?(Array)
77
+ value = value.each do |grant_type|
78
+ unless metadata[:grant_types_supported].include?(grant_type)
79
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_grant_type_message(grant_type))
80
+ end
81
+ end.join(" ")
82
+ else
83
+ set_field_error(key, invalid_client_metadata_message)
84
+ end
85
+ key = oauth_applications_grant_types_column
86
+ when "response_types"
87
+ if value.is_a?(Array)
88
+ grant_types = request.params["grant_types"] || metadata[:grant_types_supported]
89
+ value = value.each do |response_type|
90
+ unless metadata[:response_types_supported].include?(response_type)
91
+ register_throw_json_response_error("invalid_client_metadata",
92
+ register_invalid_response_type_message(response_type))
93
+ end
94
+
95
+ validate_client_registration_response_type(response_type, grant_types)
96
+ end.join(" ")
97
+ else
98
+ set_field_error(key, invalid_client_metadata_message)
99
+ end
100
+ key = oauth_applications_response_types_column
101
+ # verify if in range and match grant type
102
+ when "client_uri", "logo_uri", "tos_uri", "policy_uri", "jwks_uri"
103
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value)) unless check_valid_uri?(value)
104
+ case key
105
+ when "client_uri"
106
+ key = "homepage_url"
107
+ when "jwks_uri"
108
+ if request.params.key?("jwks")
109
+ register_throw_json_response_error("invalid_client_metadata",
110
+ register_invalid_jwks_param_message(key, "jwks"))
111
+ end
112
+ end
113
+ key = __send__(:"oauth_applications_#{key}_column")
114
+ when "jwks"
115
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
116
+ if request.params.key?("jwks_uri")
117
+ register_throw_json_response_error("invalid_client_metadata",
118
+ register_invalid_jwks_param_message(key, "jwks_uri"))
119
+ end
120
+
121
+ key = oauth_applications_jwks_column
122
+ value = JSON.dump(value)
123
+ when "scope"
124
+ scopes = value.split(" ") - oauth_application_scopes
125
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_scopes_message(value)) unless scopes.empty?
126
+ key = oauth_applications_scopes_column
127
+ # verify if in range
128
+ when "contacts"
129
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_contacts_message(value)) unless value.is_a?(Array)
130
+ value = value.join(" ")
131
+ key = oauth_applications_contacts_column
132
+ when "client_name"
133
+ key = oauth_applications_name_column
134
+ else
135
+ if respond_to?(:"oauth_applications_#{key}_column")
136
+ property = :"oauth_applications_#{key}_column"
137
+ if PROTECTED_APPLICATION_ATTRIBUTES.include?(property)
138
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
139
+ end
140
+ key = __send__(property)
141
+ elsif !db[oauth_applications_table].columns.include?(key.to_sym)
142
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
143
+ end
144
+ end
145
+ params[key] = value
146
+ end
147
+ end
148
+
149
+ def validate_client_registration_response_type(response_type, grant_types)
150
+ case response_type
151
+ when "code"
152
+ unless grant_types.include?("authorization_code")
153
+ register_throw_json_response_error("invalid_client_metadata",
154
+ register_invalid_response_type_for_grant_type_message(response_type,
155
+ "authorization_code"))
156
+ end
157
+ when "token"
158
+ unless grant_types.include?("implicit")
159
+ register_throw_json_response_error("invalid_client_metadata",
160
+ register_invalid_response_type_for_grant_type_message(response_type, "implicit"))
161
+ end
162
+ when "none"
163
+ if grant_types.include?("implicit") || grant_types.include?("authorization_code")
164
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_response_type_message(response_type))
165
+ end
166
+ end
167
+ end
168
+
169
+ def do_register(return_params = request.params.dup)
170
+ # set defaults
171
+ create_params = @oauth_application_params
172
+ create_params[oauth_applications_scopes_column] ||= return_params["scopes"] = oauth_application_default_scope.join(" ")
173
+ create_params[oauth_applications_token_endpoint_auth_method_column] ||= begin
174
+ return_params["token_endpoint_auth_method"] = "client_secret_basic"
175
+ "client_secret_basic"
176
+ end
177
+ create_params[oauth_applications_grant_types_column] ||= begin
178
+ return_params["grant_types"] = %w[authorization_code]
179
+ "authorization_code"
180
+ end
181
+ create_params[oauth_applications_response_types_column] ||= begin
182
+ return_params["response_types"] = %w[code]
183
+ "code"
184
+ end
185
+ rescue_from_uniqueness_error do
186
+ client_id = oauth_unique_id_generator
187
+ create_params[oauth_applications_client_id_column] = client_id
188
+ return_params["client_id"] = client_id
189
+ return_params["client_id_issued_at"] = Time.now.utc.iso8601
190
+ if create_params.key?(oauth_applications_client_secret_column)
191
+ create_params[oauth_applications_client_secret_column] = secret_hash(create_params[oauth_applications_client_secret_column])
192
+ return_params.delete("client_secret")
193
+ else
194
+ client_secret = oauth_unique_id_generator
195
+ create_params[oauth_applications_client_secret_column] = secret_hash(client_secret)
196
+ return_params["client_secret"] = client_secret
197
+ return_params["client_secret_expires_at"] = 0
198
+ end
199
+ db[oauth_applications_table].insert(create_params)
200
+ end
201
+
202
+ return_params
203
+ end
204
+
205
+ def register_throw_json_response_error(code, message)
206
+ throw_json_response_error(invalid_oauth_response_status, code, message)
207
+ end
208
+
209
+ def register_required_param_message(key)
210
+ "The param '#{key}' is required by this server."
211
+ end
212
+
213
+ def register_invalid_param_message(key)
214
+ "The param '#{key}' is not supported by this server."
215
+ end
216
+
217
+ def register_invalid_contacts_message(contacts)
218
+ "The contacts '#{contacts}' are not allowed by this server."
219
+ end
220
+
221
+ def register_invalid_uri_message(uri)
222
+ "The '#{uri}' URL is not allowed by this server."
223
+ end
224
+
225
+ def register_invalid_jwks_param_message(key1, key2)
226
+ "The param '#{key1}' cannot be accepted together with param '#{key2}'."
227
+ end
228
+
229
+ def register_invalid_scopes_message(scopes)
230
+ "The given scopes (#{scopes}) are not allowed by this server."
231
+ end
232
+
233
+ def register_invalid_grant_type_message(grant_type)
234
+ "The grant type #{grant_type} is not allowed by this server."
235
+ end
236
+
237
+ def register_invalid_response_type_message(response_type)
238
+ "The response type #{response_type} is not allowed by this server."
239
+ end
240
+
241
+ def register_invalid_response_type_for_grant_type_message(response_type, grant_type)
242
+ "The grant type '#{grant_type}' must be registered for the response " \
243
+ "type '#{response_type}' to be allowed."
244
+ end
245
+
246
+ def oauth_server_metadata_body(*)
247
+ super.tap do |data|
248
+ data[:registration_endpoint] = register_url
249
+ end
250
+ end
251
+ end
252
+ end
@@ -10,22 +10,38 @@ module Rodauth
10
10
 
11
11
  # Recommended to have hmac_secret as well
12
12
 
13
- auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
13
+ auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
14
14
  auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
15
15
 
16
16
  auth_value_method :oauth_jwt_token_issuer, nil
17
17
 
18
- auth_value_method :oauth_applications_jws_jwk_column, :jws_jwk
18
+ configuration_module_eval do
19
+ define_method :oauth_applications_jws_jwk_column do
20
+ warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_column`"
21
+ oauth_applications_jwks_column
22
+ end
23
+ define_method :oauth_applications_jws_jwk_label do
24
+ warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_label`"
25
+ oauth_applications_jws_jwk_label
26
+ end
27
+ define_method :oauth_application_jws_jwk_param do
28
+ warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_param`"
29
+ oauth_applications_jwks_param
30
+ end
31
+ end
32
+
33
+ auth_value_method :oauth_applications_subject_type_column, :subject_type
19
34
  auth_value_method :oauth_applications_jwt_public_key_column, :jwt_public_key
35
+ auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
36
+ auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
37
+ auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
20
38
 
21
- translatable_method :oauth_applications_jws_jwk_label, "JSON Web Keys"
22
39
  translatable_method :oauth_applications_jwt_public_key_label, "Public key"
23
- auth_value_method :oauth_application_jws_jwk_param, :jws_jwk
24
- auth_value_method :oauth_application_jwt_public_key_param, :jwt_public_key
25
40
 
41
+ auth_value_method :oauth_jwt_keys, {}
26
42
  auth_value_method :oauth_jwt_key, nil
27
43
  auth_value_method :oauth_jwt_public_key, nil
28
- auth_value_method :oauth_jwt_algorithm, "HS256"
44
+ auth_value_method :oauth_jwt_algorithm, "RS256"
29
45
 
30
46
  auth_value_method :oauth_jwt_jwe_key, nil
31
47
  auth_value_method :oauth_jwt_jwe_public_key, nil
@@ -119,13 +135,19 @@ module Rodauth
119
135
 
120
136
  return super unless request_object && oauth_application
121
137
 
122
- jws_jwk = if (jwk = oauth_application[oauth_applications_jws_jwk_column])
123
- jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
124
- else
125
- redirect_response_error("invalid_request_object")
126
- end
138
+ if (jwks = oauth_application_jwks)
139
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
140
+ else
141
+ redirect_response_error("invalid_request_object")
142
+ end
127
143
 
128
- claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg], verify_jti: false)
144
+ request_sig_enc_opts = {
145
+ jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
146
+ jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
147
+ jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
148
+ }.compact
149
+
150
+ claims = jwt_decode(request_object, jwks: jwks, verify_jti: false, **request_sig_enc_opts)
129
151
 
130
152
  redirect_response_error("invalid_request_object") unless claims
131
153
 
@@ -208,7 +230,12 @@ module Rodauth
208
230
  end
209
231
 
210
232
  def jwt_subject(oauth_token)
211
- case oauth_jwt_subject_type
233
+ subject_type = if oauth_application
234
+ oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
235
+ else
236
+ oauth_jwt_subject_type
237
+ end
238
+ case subject_type
212
239
  when "public"
213
240
  oauth_token[oauth_tokens_account_id_column]
214
241
  when "pairwise"
@@ -216,7 +243,7 @@ module Rodauth
216
243
  application_id = oauth_token[oauth_tokens_oauth_application_id_column]
217
244
  Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
218
245
  else
219
- raise StandardError, "unexpected subject (#{oauth_jwt_subject_type})"
246
+ raise StandardError, "unexpected subject (#{subject_type})"
220
247
  end
221
248
  end
222
249
 
@@ -245,11 +272,11 @@ module Rodauth
245
272
  }
246
273
  end
247
274
 
248
- def oauth_server_metadata_body(path)
275
+ def oauth_server_metadata_body(path = nil)
249
276
  metadata = super
250
277
  metadata.merge! \
251
278
  jwks_uri: jwks_url,
252
- token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
279
+ token_endpoint_auth_signing_alg_values_supported: (oauth_jwt_keys.keys + [oauth_jwt_algorithm]).uniq
253
280
  metadata
254
281
  end
255
282
 
@@ -257,9 +284,9 @@ module Rodauth
257
284
  @_jwt_key ||= oauth_jwt_key || begin
258
285
  if oauth_application
259
286
 
260
- if (jwk = oauth_application[oauth_applications_jws_jwk_column])
261
- jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
262
- jwk
287
+ if (jwks = oauth_application_jwks)
288
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks && jwks.is_a?(String)
289
+ jwks
263
290
  else
264
291
  oauth_application[oauth_applications_jwt_public_key_column]
265
292
  end
@@ -267,6 +294,16 @@ module Rodauth
267
294
  end
268
295
  end
269
296
 
297
+ def _jwt_public_key
298
+ @_jwt_public_key ||= oauth_jwt_public_key || begin
299
+ if oauth_application
300
+ jwks || oauth_application[oauth_applications_jwt_public_key_column]
301
+ else
302
+ _jwt_key
303
+ end
304
+ end
305
+ end
306
+
270
307
  # Resource Server only!
271
308
  #
272
309
  # returns the jwks set from the authorization server.
@@ -319,44 +356,121 @@ module Rodauth
319
356
  expected_aud == aud
320
357
  end
321
358
 
359
+ def oauth_application_jwks
360
+ jwks = oauth_application[oauth_applications_jwks_column]
361
+
362
+ if jwks
363
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
364
+ return jwks
365
+ end
366
+
367
+ jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
368
+
369
+ return unless jwks_uri
370
+
371
+ jwks_uri = URI(jwks_uri)
372
+
373
+ jwks = JWKS[jwks_uri]
374
+
375
+ return jwks if jwks
376
+
377
+ JWKS.set(jwks_uri) do
378
+ http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
379
+ http.use_ssl = jwks_uri.scheme == "https"
380
+
381
+ request = Net::HTTP::Get.new(jwks_uri.request_uri)
382
+ request["accept"] = json_response_content_type
383
+ response = http.request(request)
384
+ return unless response.code.to_i == 200
385
+
386
+ # time-to-live
387
+ ttl = if response.key?("cache-control")
388
+ cache_control = response["cache-control"]
389
+ cache_control[/max-age=(\d+)/, 1].to_i
390
+ elsif response.key?("expires")
391
+ Time.parse(response["expires"]).to_i - Time.now.to_i
392
+ end
393
+
394
+ [JSON.parse(response.body, symbolize_names: true), ttl]
395
+ end
396
+ end
397
+
322
398
  if defined?(JSON::JWT)
399
+ # json-jwt
400
+
401
+ auth_value_method :oauth_jwt_algorithms_supported, %w[
402
+ HS256 HS384 HS512
403
+ RS256 RS384 RS512
404
+ PS256 PS384 PS512
405
+ ES256 ES384 ES512 ES256K
406
+ ]
407
+ auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
408
+ RSA1_5 RSA-OAEP dir A128KW A256KW
409
+ ]
410
+ auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
411
+ A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
412
+ ]
323
413
 
324
414
  def jwk_import(data)
325
415
  JSON::JWK.new(data)
326
416
  end
327
417
 
328
- # json-jwt
329
- def jwt_encode(payload)
418
+ def jwt_encode(payload,
419
+ jwks: nil,
420
+ jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
421
+ signing_algorithm: oauth_jwt_algorithm,
422
+ encryption_algorithm: oauth_jwt_jwe_algorithm,
423
+ encryption_method: oauth_jwt_jwe_encryption_method)
330
424
  payload[:jti] = generate_jti(payload)
331
425
  jwt = JSON::JWT.new(payload)
332
- jwk = JSON::JWK.new(_jwt_key)
333
426
 
334
- jwt = jwt.sign(jwk, oauth_jwt_algorithm)
427
+ key = oauth_jwt_keys[signing_algorithm] || _jwt_key
428
+ key = key.first if key.is_a?(Array)
429
+
430
+ jwk = JSON::JWK.new(key || "")
431
+
432
+ jwt = jwt.sign(jwk, signing_algorithm)
335
433
  jwt.kid = jwk.thumbprint
336
434
 
337
- if oauth_jwt_jwe_key
338
- algorithm = oauth_jwt_jwe_algorithm.to_sym if oauth_jwt_jwe_algorithm
339
- jwt = jwt.encrypt(oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
340
- algorithm,
341
- oauth_jwt_jwe_encryption_method.to_sym)
435
+ if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
436
+ jwk = JSON::JWK.new(jwk)
437
+ jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
438
+ jwe.to_s
439
+ elsif jwe_key
440
+ algorithm = encryption_algorithm.to_sym if encryption_algorithm
441
+ meth = encryption_method.to_sym if encryption_method
442
+ jwt.encrypt(jwe_key, algorithm, meth)
443
+ else
444
+ jwt.to_s
342
445
  end
343
- jwt.to_s
344
446
  end
345
447
 
346
448
  def jwt_decode(
347
449
  token,
450
+ jwks: nil,
348
451
  jws_key: oauth_jwt_public_key || _jwt_key,
452
+ jws_algorithm: oauth_jwt_algorithm,
453
+ jwe_key: oauth_jwt_jwe_key,
454
+ jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
455
+ jws_encryption_method: oauth_jwt_jwe_encryption_method,
349
456
  verify_claims: true,
350
457
  verify_jti: true,
351
458
  verify_iss: true,
352
459
  verify_aud: false,
353
460
  **
354
461
  )
355
- token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
462
+ token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if jwe_key
356
463
 
357
464
  claims = if is_authorization_server?
358
465
  if oauth_jwt_legacy_public_key
359
466
  JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
467
+ elsif jwks
468
+ enc_algs = [jws_encryption_algorithm].compact
469
+ enc_meths = [jws_encryption_method].compact
470
+ sig_algs = [jws_algorithm].compact.map(&:to_sym)
471
+ jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
472
+ jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
473
+ jws
360
474
  elsif jws_key
361
475
  JSON::JWT.decode(token, jws_key)
362
476
  end
@@ -390,20 +504,43 @@ module Rodauth
390
504
  end
391
505
 
392
506
  elsif defined?(JWT)
393
-
394
507
  # ruby-jwt
508
+ require "rodauth/oauth/jwe_extensions" if defined?(JWE)
509
+
510
+ auth_value_method :oauth_jwt_algorithms_supported, %w[
511
+ HS256 HS384 HS512 HS512256
512
+ RS256 RS384 RS512
513
+ ED25519
514
+ ES256 ES384 ES512
515
+ PS256 PS384 PS512
516
+ ]
517
+
518
+ auth_value_methods(
519
+ :oauth_jwt_jwe_algorithms_supported,
520
+ :oauth_jwt_jwe_encryption_methods_supported
521
+ )
522
+
523
+ def oauth_jwt_jwe_algorithms_supported
524
+ JWE::VALID_ALG
525
+ end
526
+
527
+ def oauth_jwt_jwe_encryption_methods_supported
528
+ JWE::VALID_ENC
529
+ end
395
530
 
396
531
  def jwk_import(data)
397
532
  JWT::JWK.import(data).keypair
398
533
  end
399
534
 
400
- def jwt_encode(payload)
535
+ def jwt_encode(payload, signing_algorithm: oauth_jwt_algorithm)
401
536
  headers = {}
402
537
 
403
- key = _jwt_key
538
+ key = oauth_jwt_keys[signing_algorithm] || _jwt_key
539
+ key = key.first if key.is_a?(Array)
404
540
 
405
- if key.is_a?(OpenSSL::PKey::RSA)
406
- jwk = JWT::JWK.new(_jwt_key)
541
+ case key
542
+ when OpenSSL::PKey::PKey
543
+ jwk = JWT::JWK.new(key)
407
544
  headers[:kid] = jwk.kid
408
545
 
409
546
  key = jwk.keypair
@@ -411,23 +548,44 @@ module Rodauth
411
548
 
412
549
  # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
413
550
  payload[:jti] = generate_jti(payload)
414
- token = JWT.encode(payload, key, oauth_jwt_algorithm, headers)
415
-
416
- if oauth_jwt_jwe_key
417
- params = {
418
- zip: "DEF",
419
- copyright: oauth_jwt_jwe_copyright
420
- }
421
- params[:enc] = oauth_jwt_jwe_encryption_method if oauth_jwt_jwe_encryption_method
422
- params[:alg] = oauth_jwt_jwe_algorithm if oauth_jwt_jwe_algorithm
423
- token = JWE.encrypt(token, oauth_jwt_jwe_public_key || oauth_jwt_jwe_key, **params)
551
+ JWT.encode(payload, key, signing_algorithm, headers)
552
+ end
553
+
554
+ if defined?(JWE)
555
+ def jwt_encode_with_jwe(
556
+ payload,
557
+ jwks: nil,
558
+ jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
559
+ encryption_algorithm: oauth_jwt_jwe_algorithm,
560
+ encryption_method: oauth_jwt_jwe_encryption_method, **args
561
+ )
562
+
563
+ token = jwt_encode_without_jwe(payload, **args)
564
+
565
+ return token unless encryption_algorithm && encryption_method
566
+
567
+ if jwks && jwks.any? { |k| k[:use] == "enc" }
568
+ JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
569
+ elsif jwe_key
570
+ params = {
571
+ zip: "DEF",
572
+ copyright: oauth_jwt_jwe_copyright
573
+ }
574
+ params[:enc] = encryption_method if encryption_method
575
+ params[:alg] = encryption_algorithm if encryption_algorithm
576
+ JWE.encrypt(token, jwe_key, **params)
577
+ else
578
+ token
579
+ end
424
580
  end
425
581
 
426
- token
582
+ alias_method :jwt_encode_without_jwe, :jwt_encode
583
+ alias_method :jwt_encode, :jwt_encode_with_jwe
427
584
  end
428
585
 
429
586
  def jwt_decode(
430
587
  token,
588
+ jwks: nil,
431
589
  jws_key: oauth_jwt_public_key || _jwt_key,
432
590
  jws_algorithm: oauth_jwt_algorithm,
433
591
  verify_claims: true,
@@ -435,9 +593,6 @@ module Rodauth
435
593
  verify_iss: true,
436
594
  verify_aud: false
437
595
  )
438
- # decrypt jwe
439
- token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
440
-
441
596
  # verifying the JWT implies verifying:
442
597
  #
443
598
  # issuer: check that server generated the token
@@ -465,6 +620,8 @@ module Rodauth
465
620
  if oauth_jwt_legacy_public_key
466
621
  algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
467
622
  JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
623
+ elsif jwks
624
+ JWT.decode(token, nil, true, algorithms: [jws_algorithm], jwks: { keys: jwks }, **verify_claims_params).first
468
625
  elsif jws_key
469
626
  JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
470
627
  end
@@ -480,6 +637,33 @@ module Rodauth
480
637
  nil
481
638
  end
482
639
 
640
+ if defined?(JWE)
641
+ def jwt_decode_with_jwe(
642
+ token,
643
+ jwks: nil,
644
+ jwe_key: oauth_jwt_jwe_key,
645
+ jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
646
+ jws_encryption_method: oauth_jwt_jwe_encryption_method,
647
+ **args
648
+ )
649
+
650
+ token = if jwks && jwks.any? { |k| k[:use] == "enc" }
651
+ JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
652
+ elsif jwe_key
653
+ JWE.decrypt(token, jwe_key)
654
+ else
655
+ token
656
+ end
657
+
658
+ jwt_decode_without_jwe(token, jwks: jwks, **args)
659
+ rescue JWE::DecodeError => e
660
+ jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
661
+ end
662
+
663
+ alias_method :jwt_decode_without_jwe, :jwt_decode
664
+ alias_method :jwt_decode, :jwt_decode_with_jwe
665
+ end
666
+
483
667
  def jwks_set
484
668
  @jwks_set ||= [
485
669
  (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
@@ -518,5 +702,20 @@ module Rodauth
518
702
 
519
703
  super
520
704
  end
705
+
706
+ def jwt_response_success(jwt, cache = false)
707
+ response.status = 200
708
+ response["Content-Type"] ||= "application/jwt"
709
+ if cache
710
+ # defaulting to 1-day for everyone, for now at least
711
+ max_age = 60 * 60 * 24
712
+ response["Cache-Control"] = "private, max-age=#{max_age}"
713
+ else
714
+ response["Cache-Control"] = "no-store"
715
+ response["Pragma"] = "no-cache"
716
+ end
717
+ response.write(jwt)
718
+ request.halt
719
+ end
521
720
  end
522
721
  end