rodauth-oauth 0.10.4 → 1.0.0.pre.beta1

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