rodauth-oauth 0.10.4 → 1.0.0.pre.beta2

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 (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