rodauth-oauth 0.10.4 → 1.0.0.pre.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/MIGRATION-GUIDE-v1.md +286 -0
  3. data/README.md +28 -35
  4. data/doc/release_notes/1_0_0_beta1.md +38 -0
  5. data/doc/release_notes/1_0_0_beta2.md +34 -0
  6. data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +21 -11
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
  13. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
  14. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
  15. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +57 -57
  16. data/lib/rodauth/features/oauth_application_management.rb +61 -74
  17. data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
  18. data/lib/rodauth/features/oauth_authorization_code_grant.rb +62 -90
  19. data/lib/rodauth/features/oauth_authorize_base.rb +115 -22
  20. data/lib/rodauth/features/oauth_base.rb +397 -315
  21. data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
  22. data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
  23. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +52 -31
  24. data/lib/rodauth/features/oauth_grant_management.rb +70 -0
  25. data/lib/rodauth/features/oauth_implicit_grant.rb +29 -27
  26. data/lib/rodauth/features/oauth_jwt.rb +53 -689
  27. data/lib/rodauth/features/oauth_jwt_base.rb +458 -0
  28. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +48 -17
  29. data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
  30. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +116 -0
  31. data/lib/rodauth/features/oauth_management_base.rb +2 -0
  32. data/lib/rodauth/features/oauth_pkce.rb +22 -26
  33. data/lib/rodauth/features/oauth_resource_indicators.rb +33 -25
  34. data/lib/rodauth/features/oauth_resource_server.rb +59 -0
  35. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +7 -1
  36. data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
  37. data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
  38. data/lib/rodauth/features/oidc.rb +382 -241
  39. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +127 -51
  40. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +115 -0
  41. data/lib/rodauth/oauth/database_extensions.rb +8 -6
  42. data/lib/rodauth/oauth/http_extensions.rb +74 -0
  43. data/lib/rodauth/oauth/railtie.rb +20 -0
  44. data/lib/rodauth/oauth/ttl_store.rb +2 -0
  45. data/lib/rodauth/oauth/version.rb +1 -1
  46. data/lib/rodauth/oauth.rb +29 -1
  47. data/locales/en.yml +34 -22
  48. data/locales/pt.yml +34 -22
  49. data/templates/authorize.str +19 -17
  50. data/templates/device_search.str +1 -1
  51. data/templates/device_verification.str +2 -2
  52. data/templates/jwks_field.str +1 -0
  53. data/templates/new_oauth_application.str +1 -2
  54. data/templates/oauth_application.str +2 -2
  55. data/templates/oauth_application_oauth_grants.str +54 -0
  56. data/templates/oauth_applications.str +2 -2
  57. data/templates/oauth_grants.str +52 -0
  58. metadata +23 -16
  59. data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
  60. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
  61. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
  62. data/lib/rodauth/features/oauth.rb +0 -9
  63. data/lib/rodauth/features/oauth_http_mac.rb +0 -86
  64. data/lib/rodauth/features/oauth_token_management.rb +0 -81
  65. data/lib/rodauth/oauth/refinements.rb +0 -48
  66. data/templates/jwt_public_key_field.str +0 -4
  67. data/templates/oauth_application_oauth_tokens.str +0 -52
  68. data/templates/oauth_tokens.str +0 -50
@@ -0,0 +1,458 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth"
4
+ require "rodauth/oauth/http_extensions"
5
+
6
+ module Rodauth
7
+ Feature.define(:oauth_jwt_base, :OauthJwtBase) do
8
+ depends :oauth_base
9
+
10
+ auth_value_method :oauth_application_jwt_public_key_param, "jwt_public_key"
11
+ auth_value_method :oauth_application_jwks_param, "jwks"
12
+
13
+ auth_value_method :oauth_jwt_keys, {}
14
+ auth_value_method :oauth_jwt_public_keys, {}
15
+
16
+ auth_value_method :oauth_jwt_jwe_keys, {}
17
+ auth_value_method :oauth_jwt_jwe_public_keys, {}
18
+
19
+ auth_value_method :oauth_jwt_jwe_copyright, nil
20
+
21
+ auth_value_methods(
22
+ :jwt_encode,
23
+ :jwt_decode,
24
+ :jwt_decode_no_key,
25
+ :generate_jti,
26
+ :oauth_jwt_issuer,
27
+ :oauth_jwt_audience
28
+ )
29
+
30
+ private
31
+
32
+ def oauth_jwt_issuer
33
+ # The JWT MUST contain an "iss" (issuer) claim that contains a
34
+ # unique identifier for the entity that issued the JWT.
35
+ @oauth_jwt_issuer ||= authorization_server_url
36
+ end
37
+
38
+ def oauth_jwt_audience
39
+ # The JWT MUST contain an "aud" (audience) claim containing a
40
+ # value that identifies the authorization server as an intended
41
+ # audience. The token endpoint URL of the authorization server
42
+ # MAY be used as a value for an "aud" element to identify the
43
+ # authorization server as an intended audience of the JWT.
44
+ @oauth_jwt_audience ||= if is_authorization_server?
45
+ oauth_application[oauth_applications_client_id_column]
46
+ else
47
+ metadata = authorization_server_metadata
48
+
49
+ return unless metadata
50
+
51
+ metadata[:token_endpoint]
52
+ end
53
+ end
54
+
55
+ def grant_from_application?(grant_or_claims, oauth_application)
56
+ return super if grant_or_claims[oauth_grants_id_column]
57
+
58
+ if grant_or_claims["client_id"]
59
+ grant_or_claims["client_id"] == oauth_application[oauth_applications_client_id_column]
60
+ else
61
+ Array(grant_or_claims["aud"]).include?(oauth_application[oauth_applications_client_id_column])
62
+ end
63
+ end
64
+
65
+ def jwt_subject(oauth_grant, client_application = oauth_application)
66
+ account_id = oauth_grant[oauth_grants_account_id_column]
67
+
68
+ return account_id.to_s if account_id
69
+
70
+ client_application[oauth_applications_client_id_column]
71
+ end
72
+
73
+ def oauth_server_metadata_body(path = nil)
74
+ metadata = super
75
+ metadata.merge! \
76
+ token_endpoint_auth_signing_alg_values_supported: oauth_jwt_keys.keys.uniq
77
+ metadata
78
+ end
79
+
80
+ def _jwt_key
81
+ @_jwt_key ||= (oauth_application_jwks(oauth_application) if oauth_application)
82
+ end
83
+
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
+ # Resource Server only!
93
+ #
94
+ # returns the jwks set from the authorization server.
95
+ def auth_server_jwks_set
96
+ metadata = authorization_server_metadata
97
+
98
+ return unless metadata && (jwks_uri = metadata[:jwks_uri])
99
+
100
+ jwks_uri = URI(jwks_uri)
101
+
102
+ http_request_with_cache(jwks_uri)
103
+ end
104
+
105
+ def generate_jti(payload)
106
+ # Use the key and iat to create a unique key per request to prevent replay attacks
107
+ jti_raw = [
108
+ payload[:aud] || payload["aud"],
109
+ payload[:iat] || payload["iat"]
110
+ ].join(":").to_s
111
+ Digest::SHA256.hexdigest(jti_raw)
112
+ end
113
+
114
+ def verify_jti(jti, claims)
115
+ generate_jti(claims) == jti
116
+ end
117
+
118
+ def verify_aud(expected_aud, aud)
119
+ expected_aud == aud
120
+ end
121
+
122
+ def oauth_application_jwks(oauth_application)
123
+ jwks = oauth_application[oauth_applications_jwks_column]
124
+
125
+ if jwks
126
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
127
+ return jwks
128
+ end
129
+
130
+ jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
131
+
132
+ return unless jwks_uri
133
+
134
+ jwks_uri = URI(jwks_uri)
135
+
136
+ http_request_with_cache(jwks_uri)
137
+ end
138
+
139
+ if defined?(JSON::JWT)
140
+ # json-jwt
141
+
142
+ auth_value_method :oauth_jwt_jws_algorithms_supported, %w[
143
+ HS256 HS384 HS512
144
+ RS256 RS384 RS512
145
+ PS256 PS384 PS512
146
+ ES256 ES384 ES512 ES256K
147
+ ]
148
+ auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
149
+ RSA1_5 RSA-OAEP dir A128KW A256KW
150
+ ]
151
+ auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
152
+ A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
153
+ ]
154
+
155
+ def jwk_export(key)
156
+ JSON::JWK.new(key)
157
+ end
158
+
159
+ def jwt_encode(payload,
160
+ jwks: nil,
161
+ encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
162
+ encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
163
+ jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm,
164
+ encryption_method]],
165
+ signing_algorithm: oauth_jwt_keys.keys.first)
166
+ payload[:jti] = generate_jti(payload)
167
+ jwt = JSON::JWT.new(payload)
168
+
169
+ key = oauth_jwt_keys[signing_algorithm] || _jwt_key
170
+ key = key.first if key.is_a?(Array)
171
+
172
+ jwk = JSON::JWK.new(key || "")
173
+
174
+ jwt = jwt.sign(jwk, signing_algorithm)
175
+ jwt.kid = jwk.thumbprint
176
+
177
+ if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
178
+ jwk = JSON::JWK.new(jwk)
179
+ jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
180
+ jwe.to_s
181
+ elsif jwe_key
182
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
183
+ algorithm = encryption_algorithm.to_sym if encryption_algorithm
184
+ meth = encryption_method.to_sym if encryption_method
185
+ jwt.encrypt(jwe_key, algorithm, meth)
186
+ else
187
+ jwt.to_s
188
+ end
189
+ end
190
+
191
+ def jwt_decode(
192
+ token,
193
+ jwks: nil,
194
+ jws_algorithm: oauth_jwt_public_keys.keys.first || oauth_jwt_keys.keys.first,
195
+ jws_key: oauth_jwt_keys[jws_algorithm] || _jwt_key,
196
+ jws_encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
197
+ jws_encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
198
+ jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_keys.values.first,
199
+ verify_claims: true,
200
+ verify_jti: true,
201
+ verify_iss: true,
202
+ verify_aud: true,
203
+ **
204
+ )
205
+ jws_key = jws_key.first if jws_key.is_a?(Array)
206
+
207
+ if jwe_key
208
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
209
+ token = JSON::JWT.decode(token, jwe_key).plain_text
210
+ end
211
+
212
+ claims = if is_authorization_server?
213
+ if jwks
214
+ jwks = jwks[:keys] if jwks.is_a?(Hash)
215
+
216
+ enc_algs = [jws_encryption_algorithm].compact
217
+ enc_meths = [jws_encryption_method].compact
218
+
219
+ sig_algs = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
220
+ sig_algs = sig_algs.compact.map(&:to_sym)
221
+
222
+ # JWKs may be set up without a KID, when there's a single one
223
+ if jwks.size == 1 && !jwks[0][:kid]
224
+ key = jwks[0]
225
+ jwk_key = JSON::JWK.new(key)
226
+ jws = JSON::JWT.decode(token, jwk_key)
227
+ else
228
+ jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
229
+ jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
230
+ end
231
+ jws
232
+ elsif jws_key
233
+ JSON::JWT.decode(token, jws_key)
234
+ end
235
+ elsif (jwks = auth_server_jwks_set)
236
+ JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
237
+ end
238
+
239
+ now = Time.now
240
+ if verify_claims && (
241
+ (!claims[:exp] || Time.at(claims[:exp]) < now) &&
242
+ (claims[:nbf] && Time.at(claims[:nbf]) < now) &&
243
+ (claims[:iat] && Time.at(claims[:iat]) < now) &&
244
+ (verify_iss && claims[:iss] != oauth_jwt_issuer) &&
245
+ (verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
246
+ (verify_jti && !verify_jti(claims[:jti], claims))
247
+ )
248
+ return
249
+ end
250
+
251
+ claims
252
+ rescue JSON::JWT::Exception
253
+ nil
254
+ end
255
+
256
+ def jwt_decode_no_key(token)
257
+ jws = JSON::JWT.decode(token, :skip_verification)
258
+ [jws.to_h, jws.header]
259
+ end
260
+ elsif defined?(JWT)
261
+ # ruby-jwt
262
+ require "rodauth/oauth/jwe_extensions" if defined?(JWE)
263
+
264
+ auth_value_method :oauth_jwt_jws_algorithms_supported, %w[
265
+ HS256 HS384 HS512 HS512256
266
+ RS256 RS384 RS512
267
+ ED25519
268
+ ES256 ES384 ES512
269
+ PS256 PS384 PS512
270
+ ]
271
+
272
+ if defined?(JWE)
273
+ auth_value_methods(
274
+ :oauth_jwt_jwe_algorithms_supported,
275
+ :oauth_jwt_jwe_encryption_methods_supported
276
+ )
277
+
278
+ def oauth_jwt_jwe_algorithms_supported
279
+ JWE::VALID_ALG
280
+ end
281
+
282
+ def oauth_jwt_jwe_encryption_methods_supported
283
+ JWE::VALID_ENC
284
+ end
285
+ else
286
+ auth_value_method :oauth_jwt_jwe_algorithms_supported, []
287
+ auth_value_method :oauth_jwt_jwe_encryption_methods_supported, []
288
+ end
289
+
290
+ def jwk_export(key)
291
+ JWT::JWK.new(key).export
292
+ end
293
+
294
+ def jwt_encode(payload,
295
+ signing_algorithm: oauth_jwt_keys.keys.first, **)
296
+ headers = {}
297
+
298
+ key = oauth_jwt_keys[signing_algorithm] || _jwt_key
299
+ key = key.first if key.is_a?(Array)
300
+
301
+ case key
302
+ when OpenSSL::PKey::PKey
303
+ jwk = JWT::JWK.new(key)
304
+ headers[:kid] = jwk.kid
305
+
306
+ key = jwk.keypair
307
+ end
308
+
309
+ # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
310
+ payload[:jti] = generate_jti(payload)
311
+ JWT.encode(payload, key, signing_algorithm, headers)
312
+ end
313
+
314
+ if defined?(JWE)
315
+ def jwt_encode_with_jwe(
316
+ payload,
317
+ jwks: nil,
318
+ encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
319
+ encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
320
+ jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm, encryption_method]],
321
+ **args
322
+ )
323
+ token = jwt_encode_without_jwe(payload, **args)
324
+
325
+ return token unless encryption_algorithm && encryption_method
326
+
327
+ if jwks && jwks.any? { |k| k[:use] == "enc" }
328
+ JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
329
+ elsif jwe_key
330
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
331
+ params = {
332
+ zip: "DEF",
333
+ copyright: oauth_jwt_jwe_copyright
334
+ }
335
+ params[:enc] = encryption_method if encryption_method
336
+ params[:alg] = encryption_algorithm if encryption_algorithm
337
+ JWE.encrypt(token, jwe_key, **params)
338
+ else
339
+ token
340
+ end
341
+ end
342
+
343
+ alias_method :jwt_encode_without_jwe, :jwt_encode
344
+ alias_method :jwt_encode, :jwt_encode_with_jwe
345
+ end
346
+
347
+ def jwt_decode(
348
+ token,
349
+ jwks: nil,
350
+ jws_algorithm: oauth_jwt_public_keys.keys.first || oauth_jwt_keys.keys.first,
351
+ jws_key: oauth_jwt_keys[jws_algorithm] || _jwt_key,
352
+ verify_claims: true,
353
+ verify_jti: true,
354
+ verify_iss: true,
355
+ verify_aud: true
356
+ )
357
+ jws_key = jws_key.first if jws_key.is_a?(Array)
358
+
359
+ # verifying the JWT implies verifying:
360
+ #
361
+ # issuer: check that server generated the token
362
+ # aud: check the audience field (client is who he says he is)
363
+ # iat: check that the token didn't expire
364
+ #
365
+ # subject can't be verified automatically without having access to the account id,
366
+ # which we don't because that's the whole point.
367
+ #
368
+ verify_claims_params = if verify_claims
369
+ {
370
+ verify_iss: verify_iss,
371
+ iss: oauth_jwt_issuer,
372
+ # can't use stock aud verification, as it's dependent on the client application id
373
+ verify_aud: false,
374
+ verify_jti: (verify_jti ? method(:verify_jti) : false),
375
+ verify_iat: true
376
+ }
377
+ else
378
+ {}
379
+ end
380
+
381
+ # decode jwt
382
+ claims = if is_authorization_server?
383
+ if jwks
384
+ jwks = jwks[:keys] if jwks.is_a?(Hash)
385
+
386
+ # JWKs may be set up without a KID, when there's a single one
387
+ if jwks.size == 1 && !jwks[0][:kid]
388
+ key = jwks[0]
389
+ algo = key[:alg]
390
+ key = JWT::JWK.import(key).keypair
391
+ JWT.decode(token, key, true, algorithms: [algo], **verify_claims_params).first
392
+ else
393
+ algorithms = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
394
+ JWT.decode(token, nil, true, algorithms: algorithms, jwks: { keys: jwks }, **verify_claims_params).first
395
+ end
396
+ elsif jws_key
397
+ JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
398
+ end
399
+ elsif (jwks = auth_server_jwks_set)
400
+ algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
401
+ JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
402
+ end
403
+
404
+ return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
405
+
406
+ claims
407
+ rescue JWT::DecodeError, JWT::JWKError
408
+ nil
409
+ end
410
+
411
+ if defined?(JWE)
412
+ def jwt_decode_with_jwe(
413
+ token,
414
+ jwks: nil,
415
+ jws_encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
416
+ jws_encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
417
+ jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_keys.values.first,
418
+ **args
419
+ )
420
+
421
+ token = if jwks && jwks.any? { |k| k[:use] == "enc" }
422
+ JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
423
+ elsif jwe_key
424
+ jwe_key = jwe_key.first if jwe_key.is_a?(Array)
425
+ JWE.decrypt(token, jwe_key)
426
+ else
427
+ token
428
+ end
429
+
430
+ jwt_decode_without_jwe(token, jwks: jwks, **args)
431
+ rescue JWE::DecodeError => e
432
+ jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
433
+ end
434
+
435
+ alias_method :jwt_decode_without_jwe, :jwt_decode
436
+ alias_method :jwt_decode, :jwt_decode_with_jwe
437
+ end
438
+
439
+ def jwt_decode_no_key(token)
440
+ JWT.decode(token, nil, false)
441
+ end
442
+ else
443
+ # :nocov:
444
+ def jwk_export(_key)
445
+ raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
446
+ end
447
+
448
+ def jwt_encode(_token)
449
+ raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
450
+ end
451
+
452
+ def jwt_decode(_token, **)
453
+ raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
454
+ end
455
+ # :nocov:
456
+ end
457
+ end
458
+ end
@@ -1,18 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rodauth/oauth/version"
4
- require "rodauth/oauth/ttl_store"
3
+ require "rodauth/oauth"
5
4
 
6
5
  module Rodauth
7
6
  Feature.define(:oauth_jwt_bearer_grant, :OauthJwtBearerGrant) do
8
7
  depends :oauth_assertion_base, :oauth_jwt
9
8
 
9
+ auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
10
+
10
11
  auth_value_methods(
11
12
  :require_oauth_application_from_jwt_bearer_assertion_issuer,
12
13
  :require_oauth_application_from_jwt_bearer_assertion_subject,
13
14
  :account_from_jwt_bearer_assertion
14
15
  )
15
16
 
17
+ def oauth_token_endpoint_auth_methods_supported
18
+ if oauth_applications_client_secret_hash_column.nil?
19
+ super | %w[client_secret_jwt private_key_jwt urn:ietf:params:oauth:client-assertion-type:jwt-bearer]
20
+ else
21
+ super | %w[private_key_jwt]
22
+ end
23
+ end
24
+
25
+ def oauth_grant_types_supported
26
+ super | %w[urn:ietf:params:oauth:grant-type:jwt-bearer]
27
+ end
28
+
16
29
  private
17
30
 
18
31
  def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
@@ -26,13 +39,37 @@ module Rodauth
26
39
  end
27
40
 
28
41
  def require_oauth_application_from_jwt_bearer_assertion_subject(assertion)
29
- claims = jwt_assertion(assertion)
42
+ claims, header = jwt_decode_no_key(assertion)
43
+
44
+ client_id = claims["sub"]
45
+
46
+ case header["alg"]
47
+ when "none"
48
+ # do not accept jwts with no alg set
49
+ authorization_required
50
+ when /\AHS/
51
+ require_oauth_application_from_client_secret_jwt(client_id, assertion, header["alg"])
52
+ else
53
+ require_oauth_application_from_private_key_jwt(client_id, assertion)
54
+ end
55
+ end
30
56
 
31
- return unless claims
57
+ def require_oauth_application_from_client_secret_jwt(client_id, assertion, alg)
58
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
59
+ authorization_required unless oauth_application && supports_auth_method?(oauth_application, "client_secret_jwt")
60
+ client_secret = oauth_application[oauth_applications_client_secret_column]
61
+ claims = jwt_assertion(assertion, jws_key: client_secret, jws_algorithm: alg)
62
+ authorization_required unless claims && claims["iss"] == client_id
63
+ oauth_application
64
+ end
32
65
 
33
- db[oauth_applications_table].where(
34
- oauth_applications_client_id_column => claims["sub"]
35
- ).first
66
+ def require_oauth_application_from_private_key_jwt(client_id, assertion)
67
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
68
+ authorization_required unless oauth_application && supports_auth_method?(oauth_application, "private_key_jwt")
69
+ jwks = oauth_application_jwks(oauth_application)
70
+ claims = jwt_assertion(assertion, jwks: jwks)
71
+ authorization_required unless claims
72
+ oauth_application
36
73
  end
37
74
 
38
75
  def account_from_jwt_bearer_assertion(assertion)
@@ -43,18 +80,12 @@ module Rodauth
43
80
  account_from_bearer_assertion_subject(claims["sub"])
44
81
  end
45
82
 
46
- def jwt_assertion(assertion)
47
- claims = jwt_decode(assertion, verify_iss: false, verify_aud: false)
48
- return unless verify_aud(token_url, claims["aud"])
83
+ def jwt_assertion(assertion, **kwargs)
84
+ claims = jwt_decode(assertion, verify_iss: false, verify_aud: false, verify_jti: false, **kwargs)
49
85
 
50
- claims
51
- end
86
+ return unless claims && verify_aud(request.url, claims["aud"])
52
87
 
53
- def oauth_server_metadata_body(*)
54
- super.tap do |data|
55
- data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:jwt-bearer"
56
- data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
57
- end
88
+ claims
58
89
  end
59
90
  end
60
91
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth"
4
+ require "rodauth/oauth/http_extensions"
5
+
6
+ module Rodauth
7
+ Feature.define(:oauth_jwt_jwks, :OauthJwtJwks) do
8
+ depends :oauth_jwt_base
9
+
10
+ auth_value_methods(:jwks_set)
11
+
12
+ auth_server_route(:jwks) do |r|
13
+ before_jwks_route
14
+
15
+ r.get do
16
+ json_response_success({ keys: jwks_set }, true)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def oauth_server_metadata_body(path = nil)
23
+ metadata = super
24
+ metadata.merge!(jwks_uri: jwks_url)
25
+ metadata
26
+ end
27
+
28
+ def jwks_set
29
+ @jwks_set ||= [
30
+ *(
31
+ unless oauth_jwt_public_keys.empty?
32
+ oauth_jwt_public_keys.flat_map { |algo, pkeys| Array(pkeys).map { |pkey| jwk_export(pkey).merge(use: "sig", alg: algo) } }
33
+ end
34
+ ),
35
+ *(
36
+ unless oauth_jwt_jwe_public_keys.empty?
37
+ oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
38
+ Array(pkeys).map do |pkey|
39
+ jwk_export(pkey).merge(use: "enc", alg: algo)
40
+ end
41
+ end
42
+ end
43
+ )
44
+ ].compact
45
+ end
46
+ end
47
+ end