rodauth-oauth 0.10.4 → 1.0.0.pre.beta1

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