rodauth-oauth 0.10.3 → 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 (68) 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/0_10_3.md +1 -1
  5. data/doc/release_notes/0_10_4.md +11 -0
  6. data/doc/release_notes/1_0_0_beta1.md +38 -0
  7. data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +4 -6
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
  13. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
  14. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
  15. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
  16. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +18 -29
  17. data/lib/rodauth/features/oauth_application_management.rb +59 -72
  18. data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
  19. data/lib/rodauth/features/oauth_authorization_code_grant.rb +35 -88
  20. data/lib/rodauth/features/oauth_authorize_base.rb +103 -20
  21. data/lib/rodauth/features/oauth_base.rb +365 -302
  22. data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
  23. data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
  24. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +46 -28
  25. data/lib/rodauth/features/oauth_grant_management.rb +70 -0
  26. data/lib/rodauth/features/oauth_implicit_grant.rb +25 -24
  27. data/lib/rodauth/features/oauth_jwt.rb +52 -678
  28. data/lib/rodauth/features/oauth_jwt_base.rb +435 -0
  29. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +45 -17
  30. data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
  31. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +62 -0
  32. data/lib/rodauth/features/oauth_management_base.rb +2 -0
  33. data/lib/rodauth/features/oauth_pkce.rb +22 -26
  34. data/lib/rodauth/features/oauth_resource_indicators.rb +39 -22
  35. data/lib/rodauth/features/oauth_resource_server.rb +38 -0
  36. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +5 -1
  37. data/lib/rodauth/features/oauth_token_introspection.rb +76 -45
  38. data/lib/rodauth/features/oauth_token_revocation.rb +46 -31
  39. data/lib/rodauth/features/oidc.rb +188 -95
  40. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +89 -53
  41. data/lib/rodauth/oauth/database_extensions.rb +8 -6
  42. data/lib/rodauth/oauth/http_extensions.rb +61 -0
  43. data/lib/rodauth/oauth/railtie.rb +20 -0
  44. data/lib/rodauth/oauth/version.rb +1 -1
  45. data/lib/rodauth/oauth.rb +29 -1
  46. data/locales/en.yml +32 -22
  47. data/locales/pt.yml +32 -22
  48. data/templates/authorize.str +19 -24
  49. data/templates/device_search.str +1 -1
  50. data/templates/device_verification.str +2 -2
  51. data/templates/jwks_field.str +1 -0
  52. data/templates/new_oauth_application.str +1 -2
  53. data/templates/oauth_application.str +2 -2
  54. data/templates/oauth_application_oauth_grants.str +54 -0
  55. data/templates/oauth_applications.str +2 -2
  56. data/templates/oauth_grants.str +52 -0
  57. metadata +21 -17
  58. data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
  59. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
  60. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
  61. data/lib/rodauth/features/oauth.rb +0 -9
  62. data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
  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
@@ -1,89 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rodauth/oauth/version"
4
- require "rodauth/oauth/ttl_store"
3
+ require "rodauth/oauth"
4
+ require "rodauth/oauth/http_extensions"
5
5
 
6
6
  module Rodauth
7
7
  Feature.define(:oauth_jwt, :OauthJwt) do
8
- depends :oauth
8
+ depends :oauth_jwt_base, :oauth_jwt_jwks
9
9
 
10
- JWKS = OAuth::TtlStore.new
11
-
12
- # Recommended to have hmac_secret as well
13
-
14
- auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
15
- auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
16
-
17
- auth_value_method :oauth_jwt_token_issuer, nil
18
-
19
- configuration_module_eval do
20
- define_method :oauth_applications_jws_jwk_column do
21
- warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_column`"
22
- oauth_applications_jwks_column
23
- end
24
- define_method :oauth_applications_jws_jwk_label do
25
- warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_label`"
26
- oauth_applications_jws_jwk_label
27
- end
28
- define_method :oauth_application_jws_jwk_param do
29
- warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_param`"
30
- oauth_applications_jwks_param
31
- end
32
- end
33
-
34
- auth_value_method :oauth_applications_subject_type_column, :subject_type
35
- auth_value_method :oauth_applications_jwt_public_key_column, :jwt_public_key
36
- auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
37
- auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
38
- auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
39
-
40
- translatable_method :oauth_applications_jwt_public_key_label, "Public key"
41
-
42
- auth_value_method :oauth_application_jwt_public_key_param, "jwt_public_key"
43
- auth_value_method :oauth_application_jwks_param, "jwks"
44
-
45
- auth_value_method :oauth_jwt_keys, {}
46
- auth_value_method :oauth_jwt_key, nil
47
- auth_value_method :oauth_jwt_public_keys, {}
48
- auth_value_method :oauth_jwt_public_key, nil
49
- auth_value_method :oauth_jwt_algorithm, "RS256"
50
-
51
- auth_value_method :oauth_jwt_jwe_keys, {}
52
- auth_value_method :oauth_jwt_jwe_key, nil
53
- auth_value_method :oauth_jwt_jwe_public_keys, {}
54
- auth_value_method :oauth_jwt_jwe_public_key, nil
55
- auth_value_method :oauth_jwt_jwe_algorithm, nil
56
- auth_value_method :oauth_jwt_jwe_encryption_method, nil
57
-
58
- # values used for rotating keys
59
- auth_value_method :oauth_jwt_legacy_public_key, nil
60
- auth_value_method :oauth_jwt_legacy_algorithm, nil
61
-
62
- auth_value_method :oauth_jwt_jwe_copyright, nil
63
- auth_value_method :oauth_jwt_audience, nil
64
-
65
- translatable_method :request_uri_not_supported_message, "request uri is unsupported"
66
- translatable_method :invalid_request_object_message, "request object is invalid"
67
-
68
- auth_value_methods(
69
- :jwt_encode,
70
- :jwt_decode,
71
- :jwks_set,
72
- :generate_jti
73
- )
74
-
75
- route(:jwks) do |r|
76
- next unless is_authorization_server?
77
-
78
- r.get do
79
- json_response_success({ keys: jwks_set }, true)
80
- end
81
- end
10
+ auth_value_method :oauth_jwt_access_tokens, true
82
11
 
83
12
  def require_oauth_authorization(*scopes)
84
- authorization_required unless authorization_token
13
+ return super unless oauth_jwt_access_tokens
85
14
 
86
- scopes << oauth_application_default_scope if scopes.empty?
15
+ authorization_required unless authorization_token
87
16
 
88
17
  token_scopes = authorization_token["scope"].split(" ")
89
18
 
@@ -91,18 +20,32 @@ module Rodauth
91
20
  end
92
21
 
93
22
  def oauth_token_subject
23
+ return super unless oauth_jwt_access_tokens
24
+
94
25
  return unless authorization_token
95
26
 
96
27
  authorization_token["sub"]
97
28
  end
98
29
 
99
- private
30
+ def current_oauth_account
31
+ subject = oauth_token_subject
32
+
33
+ return if subject == authorization_token["client_id"]
34
+
35
+ oauth_account_ds(subject).first
36
+ end
100
37
 
101
- def issuer
102
- @issuer ||= oauth_jwt_token_issuer || authorization_server_url
38
+ def current_oauth_application
39
+ db[oauth_applications_table].where(
40
+ oauth_applications_client_id_column => authorization_token["client_id"]
41
+ ).first
103
42
  end
104
43
 
44
+ private
45
+
105
46
  def authorization_token
47
+ return super unless oauth_jwt_access_tokens
48
+
106
49
  return @authorization_token if defined?(@authorization_token)
107
50
 
108
51
  @authorization_token = begin
@@ -110,97 +53,58 @@ module Rodauth
110
53
 
111
54
  return unless bearer_token
112
55
 
113
- jwt_token = jwt_decode(bearer_token)
114
-
115
- return unless jwt_token
116
-
117
- return if jwt_token["iss"] != issuer ||
118
- (oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
119
- !jwt_token["sub"]
120
-
121
- jwt_token
122
- end
123
- end
124
-
125
- # /authorize
56
+ jwt_claims = jwt_decode(bearer_token)
126
57
 
127
- def validate_authorize_params
128
- # TODO: add support for requst_uri
129
- redirect_response_error("request_uri_not_supported") if param_or_nil("request_uri")
58
+ return unless jwt_claims
130
59
 
131
- request_object = param_or_nil("request")
60
+ return unless jwt_claims["sub"]
132
61
 
133
- return super unless request_object && oauth_application
62
+ return unless jwt_claims["aud"]
134
63
 
135
- if (jwks = oauth_application_jwks)
136
- jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
137
- else
138
- redirect_response_error("invalid_request_object")
64
+ jwt_claims
139
65
  end
140
-
141
- request_sig_enc_opts = {
142
- jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
143
- jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
144
- jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
145
- }.compact
146
-
147
- claims = jwt_decode(request_object, jwks: jwks, verify_jti: false, **request_sig_enc_opts)
148
-
149
- redirect_response_error("invalid_request_object") unless claims
150
-
151
- # If signed, the Authorization Request
152
- # Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
153
- # as members, with their semantics being the same as defined in the JWT
154
- # [RFC7519] specification. The value of "aud" should be the value of
155
- # the Authorization Server (AS) "issuer" as defined in RFC8414
156
- # [RFC8414].
157
- claims.delete("iss")
158
- audience = claims.delete("aud")
159
-
160
- redirect_response_error("invalid_request_object") if audience && audience != issuer
161
-
162
- claims.each do |k, v|
163
- request.params[k.to_s] = v
164
- end
165
-
166
- super
167
66
  end
168
67
 
169
68
  # /token
170
69
 
171
- def create_oauth_token_from_token(oauth_token, update_params)
172
- otoken = super
173
- access_token = _generate_jwt_access_token(otoken)
174
- otoken[oauth_tokens_token_column] = access_token
175
- otoken
70
+ def create_token_from_token(_grant, update_params)
71
+ oauth_grant = super
72
+
73
+ if oauth_jwt_access_tokens
74
+ access_token = _generate_jwt_access_token(oauth_grant)
75
+ oauth_grant[oauth_grants_token_column] = access_token
76
+ end
77
+ oauth_grant
176
78
  end
177
79
 
178
- def generate_oauth_token(params = {}, should_generate_refresh_token = true)
179
- oauth_token = super
180
- access_token = _generate_jwt_access_token(oauth_token)
181
- oauth_token[oauth_tokens_token_column] = access_token
182
- oauth_token
80
+ def generate_token(_grant_params = {}, should_generate_refresh_token = true)
81
+ oauth_grant = super
82
+ if oauth_jwt_access_tokens
83
+ access_token = _generate_jwt_access_token(oauth_grant)
84
+ oauth_grant[oauth_grants_token_column] = access_token
85
+ end
86
+ oauth_grant
183
87
  end
184
88
 
185
- def _generate_jwt_access_token(oauth_token)
186
- claims = jwt_claims(oauth_token)
89
+ def _generate_jwt_access_token(oauth_grant)
90
+ claims = jwt_claims(oauth_grant)
187
91
 
188
92
  # one of the points of using jwt is avoiding database lookups, so we put here all relevant
189
93
  # token data.
190
- claims[:scope] = oauth_token[oauth_tokens_scopes_column]
94
+ claims[:scope] = oauth_grant[oauth_grants_scopes_column]
191
95
 
192
96
  jwt_encode(claims)
193
97
  end
194
98
 
195
99
  def _generate_access_token(*)
196
- # no op
100
+ return super unless oauth_jwt_access_tokens
197
101
  end
198
102
 
199
- def jwt_claims(oauth_token)
103
+ def jwt_claims(oauth_grant)
200
104
  issued_at = Time.now.to_i
201
105
 
202
106
  {
203
- iss: issuer, # issuer
107
+ iss: oauth_jwt_issuer, # issuer
204
108
  iat: issued_at, # issued at
205
109
  #
206
110
  # sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
@@ -211,542 +115,12 @@ module Rodauth
211
115
  # owner is involved, such as the client credentials grant, the value
212
116
  # of "sub" SHOULD correspond to an identifier the authorization
213
117
  # server uses to indicate the client application.
214
- sub: jwt_subject(oauth_token),
118
+ sub: jwt_subject(oauth_grant),
215
119
  client_id: oauth_application[oauth_applications_client_id_column],
216
120
 
217
- exp: issued_at + oauth_token_expires_in,
218
- aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column])
219
- }
220
- end
221
-
222
- def jwt_subject(oauth_token)
223
- subject_type = if oauth_application
224
- oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
225
- else
226
- oauth_jwt_subject_type
227
- end
228
- case subject_type
229
- when "public"
230
- oauth_token[oauth_tokens_account_id_column]
231
- when "pairwise"
232
- id = oauth_token[oauth_tokens_account_id_column]
233
- application_id = oauth_token[oauth_tokens_oauth_application_id_column]
234
- Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
235
- else
236
- raise StandardError, "unexpected subject (#{subject_type})"
237
- end
238
- end
239
-
240
- def oauth_token_by_token(token)
241
- jwt_decode(token)
242
- end
243
-
244
- def json_token_introspect_payload(oauth_token)
245
- return { active: false } unless oauth_token
246
-
247
- return super unless oauth_token["sub"] # naive check on whether it's a jwt token
248
-
249
- {
250
- active: true,
251
- scope: oauth_token["scope"],
252
- client_id: oauth_token["client_id"],
253
- # username
254
- token_type: "access_token",
255
- exp: oauth_token["exp"],
256
- iat: oauth_token["iat"],
257
- nbf: oauth_token["nbf"],
258
- sub: oauth_token["sub"],
259
- aud: oauth_token["aud"],
260
- iss: oauth_token["iss"],
261
- jti: oauth_token["jti"]
121
+ exp: issued_at + oauth_access_token_expires_in,
122
+ aud: oauth_jwt_audience
262
123
  }
263
124
  end
264
-
265
- def oauth_server_metadata_body(path = nil)
266
- metadata = super
267
- metadata.merge! \
268
- jwks_uri: jwks_url,
269
- token_endpoint_auth_signing_alg_values_supported: (oauth_jwt_keys.keys + [oauth_jwt_algorithm]).uniq
270
- metadata
271
- end
272
-
273
- def _jwt_key
274
- @_jwt_key ||= oauth_jwt_key || begin
275
- if oauth_application
276
-
277
- if (jwks = oauth_application_jwks)
278
- jwks = JSON.parse(jwks, symbolize_names: true) if jwks && jwks.is_a?(String)
279
- jwks
280
- else
281
- oauth_application[oauth_applications_jwt_public_key_column]
282
- end
283
- end
284
- end
285
- end
286
-
287
- def _jwt_public_key
288
- @_jwt_public_key ||= oauth_jwt_public_key || begin
289
- if oauth_application
290
- jwks || oauth_application[oauth_applications_jwt_public_key_column]
291
- else
292
- _jwt_key
293
- end
294
- end
295
- end
296
-
297
- # Resource Server only!
298
- #
299
- # returns the jwks set from the authorization server.
300
- def auth_server_jwks_set
301
- metadata = authorization_server_metadata
302
-
303
- return unless metadata && (jwks_uri = metadata[:jwks_uri])
304
-
305
- jwks_uri = URI(jwks_uri)
306
-
307
- jwks = JWKS[jwks_uri]
308
-
309
- return jwks if jwks
310
-
311
- JWKS.set(jwks_uri) do
312
- http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
313
- http.use_ssl = jwks_uri.scheme == "https"
314
-
315
- request = Net::HTTP::Get.new(jwks_uri.request_uri)
316
- request["accept"] = json_response_content_type
317
- response = http.request(request)
318
- authorization_required unless response.code.to_i == 200
319
-
320
- # time-to-live
321
- ttl = if response.key?("cache-control")
322
- cache_control = response["cache-control"]
323
- cache_control[/max-age=(\d+)/, 1].to_i
324
- elsif response.key?("expires")
325
- Time.parse(response["expires"]).to_i - Time.now.to_i
326
- end
327
-
328
- [JSON.parse(response.body, symbolize_names: true), ttl]
329
- end
330
- end
331
-
332
- def generate_jti(payload)
333
- # Use the key and iat to create a unique key per request to prevent replay attacks
334
- jti_raw = [
335
- payload[:aud] || payload["aud"],
336
- payload[:iat] || payload["iat"]
337
- ].join(":").to_s
338
- Digest::SHA256.hexdigest(jti_raw)
339
- end
340
-
341
- def verify_jti(jti, claims)
342
- generate_jti(claims) == jti
343
- end
344
-
345
- def verify_aud(expected_aud, aud)
346
- expected_aud == aud
347
- end
348
-
349
- def oauth_application_jwks
350
- jwks = oauth_application[oauth_applications_jwks_column]
351
-
352
- if jwks
353
- jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
354
- return jwks
355
- end
356
-
357
- jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
358
-
359
- return unless jwks_uri
360
-
361
- jwks_uri = URI(jwks_uri)
362
-
363
- jwks = JWKS[jwks_uri]
364
-
365
- return jwks if jwks
366
-
367
- JWKS.set(jwks_uri) do
368
- http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
369
- http.use_ssl = jwks_uri.scheme == "https"
370
-
371
- request = Net::HTTP::Get.new(jwks_uri.request_uri)
372
- request["accept"] = json_response_content_type
373
- response = http.request(request)
374
- return unless response.code.to_i == 200
375
-
376
- # time-to-live
377
- ttl = if response.key?("cache-control")
378
- cache_control = response["cache-control"]
379
- cache_control[/max-age=(\d+)/, 1].to_i
380
- elsif response.key?("expires")
381
- Time.parse(response["expires"]).to_i - Time.now.to_i
382
- end
383
-
384
- [JSON.parse(response.body, symbolize_names: true), ttl]
385
- end
386
- end
387
-
388
- if defined?(JSON::JWT)
389
- # json-jwt
390
-
391
- auth_value_method :oauth_jwt_algorithms_supported, %w[
392
- HS256 HS384 HS512
393
- RS256 RS384 RS512
394
- PS256 PS384 PS512
395
- ES256 ES384 ES512 ES256K
396
- ]
397
- auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
398
- RSA1_5 RSA-OAEP dir A128KW A256KW
399
- ]
400
- auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
401
- A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
402
- ]
403
-
404
- def jwk_import(data)
405
- JSON::JWK.new(data)
406
- end
407
-
408
- def jwt_encode(payload,
409
- jwks: nil,
410
- encryption_algorithm: oauth_jwt_jwe_algorithm,
411
- encryption_method: oauth_jwt_jwe_encryption_method,
412
- jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm,
413
- encryption_method]] || oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
414
- signing_algorithm: oauth_jwt_algorithm || oauth_jwt_keys.keys.first)
415
- payload[:jti] = generate_jti(payload)
416
- jwt = JSON::JWT.new(payload)
417
-
418
- key = oauth_jwt_keys[signing_algorithm] || _jwt_key
419
- key = key.first if key.is_a?(Array)
420
-
421
- jwk = JSON::JWK.new(key || "")
422
-
423
- jwt = jwt.sign(jwk, signing_algorithm)
424
- jwt.kid = jwk.thumbprint
425
-
426
- if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
427
- jwk = JSON::JWK.new(jwk)
428
- jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
429
- jwe.to_s
430
- elsif jwe_key
431
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
432
- algorithm = encryption_algorithm.to_sym if encryption_algorithm
433
- meth = encryption_method.to_sym if encryption_method
434
- jwt.encrypt(jwe_key, algorithm, meth)
435
- else
436
- jwt.to_s
437
- end
438
- end
439
-
440
- def jwt_decode(
441
- token,
442
- jwks: nil,
443
- jws_algorithm: oauth_jwt_algorithm || oauth_jwt_public_key.keys.first || oauth_jwt_keys.keys.first,
444
- jws_key: oauth_jwt_public_key || oauth_jwt_keys[jws_algorithm] || _jwt_key,
445
- jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
446
- jws_encryption_method: oauth_jwt_jwe_encryption_method,
447
- jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_key,
448
- verify_claims: true,
449
- verify_jti: true,
450
- verify_iss: true,
451
- verify_aud: false,
452
- **
453
- )
454
- jws_key = jws_key.first if jws_key.is_a?(Array)
455
-
456
- if jwe_key
457
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
458
- token = JSON::JWT.decode(token, jwe_key).plain_text
459
- end
460
-
461
- claims = if is_authorization_server?
462
- if oauth_jwt_legacy_public_key
463
- JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
464
- elsif jwks
465
- enc_algs = [jws_encryption_algorithm].compact
466
- enc_meths = [jws_encryption_method].compact
467
- sig_algs = [jws_algorithm].compact.map(&:to_sym)
468
- jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
469
- jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
470
- jws
471
- elsif jws_key
472
- JSON::JWT.decode(token, jws_key)
473
- end
474
- elsif (jwks = auth_server_jwks_set)
475
- JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
476
- end
477
-
478
- now = Time.now
479
- if verify_claims && (
480
- (!claims[:exp] || Time.at(claims[:exp]) < now) &&
481
- (claims[:nbf] && Time.at(claims[:nbf]) < now) &&
482
- (claims[:iat] && Time.at(claims[:iat]) < now) &&
483
- (verify_iss && claims[:iss] != issuer) &&
484
- (verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
485
- (verify_jti && !verify_jti(claims[:jti], claims))
486
- )
487
- return
488
- end
489
-
490
- claims
491
- rescue JSON::JWT::Exception
492
- nil
493
- end
494
-
495
- def jwks_set
496
- @jwks_set ||= [
497
- *(
498
- unless oauth_jwt_public_keys.empty?
499
- oauth_jwt_public_keys.flat_map { |algo, pkeys| pkeys.map { |pkey| JSON::JWK.new(pkey).merge(use: "sig", alg: algo) } }
500
- end
501
- ),
502
- *(
503
- unless oauth_jwt_jwe_public_keys.empty?
504
- oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
505
- pkeys.map do |pkey|
506
- JSON::JWK.new(pkey).merge(use: "enc", alg: algo)
507
- end
508
- end
509
- end
510
- ),
511
- # legacy
512
- (JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
513
- (JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key),
514
- (JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
515
- ].compact
516
- end
517
-
518
- elsif defined?(JWT)
519
- # ruby-jwt
520
- require "rodauth/oauth/jwe_extensions" if defined?(JWE)
521
-
522
- auth_value_method :oauth_jwt_algorithms_supported, %w[
523
- HS256 HS384 HS512 HS512256
524
- RS256 RS384 RS512
525
- ED25519
526
- ES256 ES384 ES512
527
- PS256 PS384 PS512
528
- ]
529
-
530
- auth_value_methods(
531
- :oauth_jwt_jwe_algorithms_supported,
532
- :oauth_jwt_jwe_encryption_methods_supported
533
- )
534
-
535
- def oauth_jwt_jwe_algorithms_supported
536
- JWE::VALID_ALG
537
- end
538
-
539
- def oauth_jwt_jwe_encryption_methods_supported
540
- JWE::VALID_ENC
541
- end
542
-
543
- def jwk_import(data)
544
- JWT::JWK.import(data).keypair
545
- end
546
-
547
- def jwt_encode(payload,
548
- signing_algorithm: oauth_jwt_algorithm || oauth_jwt_keys.keys.first)
549
- headers = {}
550
-
551
- key = oauth_jwt_keys[signing_algorithm] || _jwt_key
552
- key = key.first if key.is_a?(Array)
553
-
554
- case key
555
- when OpenSSL::PKey::PKey
556
- jwk = JWT::JWK.new(key)
557
- headers[:kid] = jwk.kid
558
-
559
- key = jwk.keypair
560
- end
561
-
562
- # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
563
- payload[:jti] = generate_jti(payload)
564
- JWT.encode(payload, key, signing_algorithm, headers)
565
- end
566
-
567
- if defined?(JWE)
568
- def jwt_encode_with_jwe(
569
- payload,
570
- jwks: nil,
571
- encryption_algorithm: oauth_jwt_jwe_algorithm,
572
- encryption_method: oauth_jwt_jwe_encryption_method,
573
- jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_keys[[encryption_algorithm, encryption_method]] || oauth_jwt_jwe_key,
574
- **args
575
- )
576
- token = jwt_encode_without_jwe(payload, **args)
577
-
578
- return token unless encryption_algorithm && encryption_method
579
-
580
- if jwks && jwks.any? { |k| k[:use] == "enc" }
581
- JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
582
- elsif jwe_key
583
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
584
- params = {
585
- zip: "DEF",
586
- copyright: oauth_jwt_jwe_copyright
587
- }
588
- params[:enc] = encryption_method if encryption_method
589
- params[:alg] = encryption_algorithm if encryption_algorithm
590
- JWE.encrypt(token, jwe_key, **params)
591
- else
592
- token
593
- end
594
- end
595
-
596
- alias_method :jwt_encode_without_jwe, :jwt_encode
597
- alias_method :jwt_encode, :jwt_encode_with_jwe
598
- end
599
-
600
- def jwt_decode(
601
- token,
602
- jwks: nil,
603
- jws_algorithm: oauth_jwt_algorithm || oauth_jwt_public_key.keys.first || oauth_jwt_keys.keys.first,
604
- jws_key: oauth_jwt_public_key || oauth_jwt_keys[jws_algorithm] || _jwt_key,
605
- verify_claims: true,
606
- verify_jti: true,
607
- verify_iss: true,
608
- verify_aud: false
609
- )
610
- jws_key = jws_key.first if jws_key.is_a?(Array)
611
-
612
- # verifying the JWT implies verifying:
613
- #
614
- # issuer: check that server generated the token
615
- # aud: check the audience field (client is who he says he is)
616
- # iat: check that the token didn't expire
617
- #
618
- # subject can't be verified automatically without having access to the account id,
619
- # which we don't because that's the whole point.
620
- #
621
- verify_claims_params = if verify_claims
622
- {
623
- verify_iss: verify_iss,
624
- iss: issuer,
625
- # can't use stock aud verification, as it's dependent on the client application id
626
- verify_aud: false,
627
- verify_jti: (verify_jti ? method(:verify_jti) : false),
628
- verify_iat: true
629
- }
630
- else
631
- {}
632
- end
633
-
634
- # decode jwt
635
- claims = if is_authorization_server?
636
- if oauth_jwt_legacy_public_key
637
- algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
638
- JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
639
- elsif jwks
640
- JWT.decode(token, nil, true, algorithms: [jws_algorithm], jwks: { keys: jwks }, **verify_claims_params).first
641
- elsif jws_key
642
- JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
643
- end
644
- elsif (jwks = auth_server_jwks_set)
645
- algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
646
- JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
647
- end
648
-
649
- return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
650
-
651
- claims
652
- rescue JWT::DecodeError, JWT::JWKError
653
- nil
654
- end
655
-
656
- if defined?(JWE)
657
- def jwt_decode_with_jwe(
658
- token,
659
- jwks: nil,
660
- jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
661
- jws_encryption_method: oauth_jwt_jwe_encryption_method,
662
- jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_key,
663
- **args
664
- )
665
-
666
- token = if jwks && jwks.any? { |k| k[:use] == "enc" }
667
- JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
668
- elsif jwe_key
669
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
670
- JWE.decrypt(token, jwe_key)
671
- else
672
- token
673
- end
674
-
675
- jwt_decode_without_jwe(token, jwks: jwks, **args)
676
- rescue JWE::DecodeError => e
677
- jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
678
- end
679
-
680
- alias_method :jwt_decode_without_jwe, :jwt_decode
681
- alias_method :jwt_decode, :jwt_decode_with_jwe
682
- end
683
-
684
- def jwks_set
685
- @jwks_set ||= [
686
- *(
687
- unless oauth_jwt_public_keys.empty?
688
- oauth_jwt_public_keys.flat_map { |algo, pkeys| pkeys.map { |pkey| JWT::JWK.new(pkey).export.merge(use: "sig", alg: algo) } }
689
- end
690
- ),
691
- *(
692
- unless oauth_jwt_jwe_public_keys.empty?
693
- oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
694
- pkeys.map do |pkey|
695
- JWT::JWK.new(pkey).export.merge(use: "enc", alg: algo)
696
- end
697
- end
698
- end
699
- ),
700
- # legacy
701
- (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
702
- (
703
- if oauth_jwt_legacy_public_key
704
- JWT::JWK.new(oauth_jwt_legacy_public_key).export.merge(use: "sig", alg: oauth_jwt_legacy_algorithm)
705
- end
706
- ),
707
- (JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
708
- ].compact
709
- end
710
- else
711
- # :nocov:
712
- def jwk_import(_data)
713
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
714
- end
715
-
716
- def jwt_encode(_token)
717
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
718
- end
719
-
720
- def jwt_decode(_token, **)
721
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
722
- end
723
-
724
- def jwks_set
725
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
726
- end
727
- # :nocov:
728
- end
729
-
730
- def validate_oauth_revoke_params
731
- token_hint = param_or_nil("token_type_hint")
732
-
733
- throw(:rodauth_error) if !token_hint || token_hint == "access_token"
734
-
735
- super
736
- end
737
-
738
- def jwt_response_success(jwt, cache = false)
739
- response.status = 200
740
- response["Content-Type"] ||= "application/jwt"
741
- if cache
742
- # defaulting to 1-day for everyone, for now at least
743
- max_age = 60 * 60 * 24
744
- response["Cache-Control"] = "private, max-age=#{max_age}"
745
- else
746
- response["Cache-Control"] = "no-store"
747
- response["Pragma"] = "no-cache"
748
- end
749
- return_response(jwt)
750
- end
751
125
  end
752
126
  end