rodauth-oauth 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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