rodauth-oauth 0.10.3 → 1.0.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/MIGRATION-GUIDE-v1.md +286 -0
  3. data/README.md +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