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
@@ -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,552 +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])
121
+ exp: issued_at + oauth_access_token_expires_in,
122
+ aud: oauth_jwt_audience
219
123
  }
220
124
  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 token_from_application?(grant_or_claims, oauth_application)
245
- return super if grant_or_claims[oauth_tokens_id_column]
246
-
247
- if grant_or_claims["client_id"]
248
- grant_or_claims["client_id"] == oauth_application[oauth_applications_client_id_column]
249
- else
250
- Array(grant_or_claims["aud"]).include?(oauth_application[oauth_applications_client_id_column])
251
- end
252
- end
253
-
254
- def json_token_introspect_payload(oauth_token)
255
- return { active: false } unless oauth_token
256
-
257
- return super unless oauth_token["sub"] # naive check on whether it's a jwt token
258
-
259
- {
260
- active: true,
261
- scope: oauth_token["scope"],
262
- client_id: oauth_token["client_id"],
263
- # username
264
- token_type: "access_token",
265
- exp: oauth_token["exp"],
266
- iat: oauth_token["iat"],
267
- nbf: oauth_token["nbf"],
268
- sub: oauth_token["sub"],
269
- aud: oauth_token["aud"],
270
- iss: oauth_token["iss"],
271
- jti: oauth_token["jti"]
272
- }
273
- end
274
-
275
- def oauth_server_metadata_body(path = nil)
276
- metadata = super
277
- metadata.merge! \
278
- jwks_uri: jwks_url,
279
- token_endpoint_auth_signing_alg_values_supported: (oauth_jwt_keys.keys + [oauth_jwt_algorithm]).uniq
280
- metadata
281
- end
282
-
283
- def _jwt_key
284
- @_jwt_key ||= oauth_jwt_key || begin
285
- if oauth_application
286
-
287
- if (jwks = oauth_application_jwks)
288
- jwks = JSON.parse(jwks, symbolize_names: true) if jwks && jwks.is_a?(String)
289
- jwks
290
- else
291
- oauth_application[oauth_applications_jwt_public_key_column]
292
- end
293
- end
294
- end
295
- end
296
-
297
- def _jwt_public_key
298
- @_jwt_public_key ||= oauth_jwt_public_key || begin
299
- if oauth_application
300
- jwks || oauth_application[oauth_applications_jwt_public_key_column]
301
- else
302
- _jwt_key
303
- end
304
- end
305
- end
306
-
307
- # Resource Server only!
308
- #
309
- # returns the jwks set from the authorization server.
310
- def auth_server_jwks_set
311
- metadata = authorization_server_metadata
312
-
313
- return unless metadata && (jwks_uri = metadata[:jwks_uri])
314
-
315
- jwks_uri = URI(jwks_uri)
316
-
317
- jwks = JWKS[jwks_uri]
318
-
319
- return jwks if jwks
320
-
321
- JWKS.set(jwks_uri) do
322
- http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
323
- http.use_ssl = jwks_uri.scheme == "https"
324
-
325
- request = Net::HTTP::Get.new(jwks_uri.request_uri)
326
- request["accept"] = json_response_content_type
327
- response = http.request(request)
328
- authorization_required unless response.code.to_i == 200
329
-
330
- # time-to-live
331
- ttl = if response.key?("cache-control")
332
- cache_control = response["cache-control"]
333
- cache_control[/max-age=(\d+)/, 1].to_i
334
- elsif response.key?("expires")
335
- Time.parse(response["expires"]).to_i - Time.now.to_i
336
- end
337
-
338
- [JSON.parse(response.body, symbolize_names: true), ttl]
339
- end
340
- end
341
-
342
- def generate_jti(payload)
343
- # Use the key and iat to create a unique key per request to prevent replay attacks
344
- jti_raw = [
345
- payload[:aud] || payload["aud"],
346
- payload[:iat] || payload["iat"]
347
- ].join(":").to_s
348
- Digest::SHA256.hexdigest(jti_raw)
349
- end
350
-
351
- def verify_jti(jti, claims)
352
- generate_jti(claims) == jti
353
- end
354
-
355
- def verify_aud(expected_aud, aud)
356
- expected_aud == aud
357
- end
358
-
359
- def oauth_application_jwks
360
- jwks = oauth_application[oauth_applications_jwks_column]
361
-
362
- if jwks
363
- jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
364
- return jwks
365
- end
366
-
367
- jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
368
-
369
- return unless jwks_uri
370
-
371
- jwks_uri = URI(jwks_uri)
372
-
373
- jwks = JWKS[jwks_uri]
374
-
375
- return jwks if jwks
376
-
377
- JWKS.set(jwks_uri) do
378
- http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
379
- http.use_ssl = jwks_uri.scheme == "https"
380
-
381
- request = Net::HTTP::Get.new(jwks_uri.request_uri)
382
- request["accept"] = json_response_content_type
383
- response = http.request(request)
384
- return unless response.code.to_i == 200
385
-
386
- # time-to-live
387
- ttl = if response.key?("cache-control")
388
- cache_control = response["cache-control"]
389
- cache_control[/max-age=(\d+)/, 1].to_i
390
- elsif response.key?("expires")
391
- Time.parse(response["expires"]).to_i - Time.now.to_i
392
- end
393
-
394
- [JSON.parse(response.body, symbolize_names: true), ttl]
395
- end
396
- end
397
-
398
- if defined?(JSON::JWT)
399
- # json-jwt
400
-
401
- auth_value_method :oauth_jwt_algorithms_supported, %w[
402
- HS256 HS384 HS512
403
- RS256 RS384 RS512
404
- PS256 PS384 PS512
405
- ES256 ES384 ES512 ES256K
406
- ]
407
- auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
408
- RSA1_5 RSA-OAEP dir A128KW A256KW
409
- ]
410
- auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
411
- A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
412
- ]
413
-
414
- def jwk_import(data)
415
- JSON::JWK.new(data)
416
- end
417
-
418
- def jwt_encode(payload,
419
- jwks: nil,
420
- encryption_algorithm: oauth_jwt_jwe_algorithm,
421
- encryption_method: oauth_jwt_jwe_encryption_method,
422
- jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm,
423
- encryption_method]] || oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
424
- signing_algorithm: oauth_jwt_algorithm || oauth_jwt_keys.keys.first)
425
- payload[:jti] = generate_jti(payload)
426
- jwt = JSON::JWT.new(payload)
427
-
428
- key = oauth_jwt_keys[signing_algorithm] || _jwt_key
429
- key = key.first if key.is_a?(Array)
430
-
431
- jwk = JSON::JWK.new(key || "")
432
-
433
- jwt = jwt.sign(jwk, signing_algorithm)
434
- jwt.kid = jwk.thumbprint
435
-
436
- if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
437
- jwk = JSON::JWK.new(jwk)
438
- jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
439
- jwe.to_s
440
- elsif jwe_key
441
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
442
- algorithm = encryption_algorithm.to_sym if encryption_algorithm
443
- meth = encryption_method.to_sym if encryption_method
444
- jwt.encrypt(jwe_key, algorithm, meth)
445
- else
446
- jwt.to_s
447
- end
448
- end
449
-
450
- def jwt_decode(
451
- token,
452
- jwks: nil,
453
- jws_algorithm: oauth_jwt_algorithm || oauth_jwt_public_key.keys.first || oauth_jwt_keys.keys.first,
454
- jws_key: oauth_jwt_public_key || oauth_jwt_keys[jws_algorithm] || _jwt_key,
455
- jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
456
- jws_encryption_method: oauth_jwt_jwe_encryption_method,
457
- jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_key,
458
- verify_claims: true,
459
- verify_jti: true,
460
- verify_iss: true,
461
- verify_aud: false,
462
- **
463
- )
464
- jws_key = jws_key.first if jws_key.is_a?(Array)
465
-
466
- if jwe_key
467
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
468
- token = JSON::JWT.decode(token, jwe_key).plain_text
469
- end
470
-
471
- claims = if is_authorization_server?
472
- if oauth_jwt_legacy_public_key
473
- JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
474
- elsif jwks
475
- enc_algs = [jws_encryption_algorithm].compact
476
- enc_meths = [jws_encryption_method].compact
477
- sig_algs = [jws_algorithm].compact.map(&:to_sym)
478
- jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
479
- jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
480
- jws
481
- elsif jws_key
482
- JSON::JWT.decode(token, jws_key)
483
- end
484
- elsif (jwks = auth_server_jwks_set)
485
- JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
486
- end
487
-
488
- now = Time.now
489
- if verify_claims && (
490
- (!claims[:exp] || Time.at(claims[:exp]) < now) &&
491
- (claims[:nbf] && Time.at(claims[:nbf]) < now) &&
492
- (claims[:iat] && Time.at(claims[:iat]) < now) &&
493
- (verify_iss && claims[:iss] != issuer) &&
494
- (verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
495
- (verify_jti && !verify_jti(claims[:jti], claims))
496
- )
497
- return
498
- end
499
-
500
- claims
501
- rescue JSON::JWT::Exception
502
- nil
503
- end
504
-
505
- def jwks_set
506
- @jwks_set ||= [
507
- *(
508
- unless oauth_jwt_public_keys.empty?
509
- oauth_jwt_public_keys.flat_map { |algo, pkeys| pkeys.map { |pkey| JSON::JWK.new(pkey).merge(use: "sig", alg: algo) } }
510
- end
511
- ),
512
- *(
513
- unless oauth_jwt_jwe_public_keys.empty?
514
- oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
515
- pkeys.map do |pkey|
516
- JSON::JWK.new(pkey).merge(use: "enc", alg: algo)
517
- end
518
- end
519
- end
520
- ),
521
- # legacy
522
- (JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
523
- (JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key),
524
- (JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
525
- ].compact
526
- end
527
-
528
- elsif defined?(JWT)
529
- # ruby-jwt
530
- require "rodauth/oauth/jwe_extensions" if defined?(JWE)
531
-
532
- auth_value_method :oauth_jwt_algorithms_supported, %w[
533
- HS256 HS384 HS512 HS512256
534
- RS256 RS384 RS512
535
- ED25519
536
- ES256 ES384 ES512
537
- PS256 PS384 PS512
538
- ]
539
-
540
- auth_value_methods(
541
- :oauth_jwt_jwe_algorithms_supported,
542
- :oauth_jwt_jwe_encryption_methods_supported
543
- )
544
-
545
- def oauth_jwt_jwe_algorithms_supported
546
- JWE::VALID_ALG
547
- end
548
-
549
- def oauth_jwt_jwe_encryption_methods_supported
550
- JWE::VALID_ENC
551
- end
552
-
553
- def jwk_import(data)
554
- JWT::JWK.import(data).keypair
555
- end
556
-
557
- def jwt_encode(payload,
558
- signing_algorithm: oauth_jwt_algorithm || oauth_jwt_keys.keys.first)
559
- headers = {}
560
-
561
- key = oauth_jwt_keys[signing_algorithm] || _jwt_key
562
- key = key.first if key.is_a?(Array)
563
-
564
- case key
565
- when OpenSSL::PKey::PKey
566
- jwk = JWT::JWK.new(key)
567
- headers[:kid] = jwk.kid
568
-
569
- key = jwk.keypair
570
- end
571
-
572
- # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
573
- payload[:jti] = generate_jti(payload)
574
- JWT.encode(payload, key, signing_algorithm, headers)
575
- end
576
-
577
- if defined?(JWE)
578
- def jwt_encode_with_jwe(
579
- payload,
580
- jwks: nil,
581
- encryption_algorithm: oauth_jwt_jwe_algorithm,
582
- encryption_method: oauth_jwt_jwe_encryption_method,
583
- jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_keys[[encryption_algorithm, encryption_method]] || oauth_jwt_jwe_key,
584
- **args
585
- )
586
- token = jwt_encode_without_jwe(payload, **args)
587
-
588
- return token unless encryption_algorithm && encryption_method
589
-
590
- if jwks && jwks.any? { |k| k[:use] == "enc" }
591
- JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
592
- elsif jwe_key
593
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
594
- params = {
595
- zip: "DEF",
596
- copyright: oauth_jwt_jwe_copyright
597
- }
598
- params[:enc] = encryption_method if encryption_method
599
- params[:alg] = encryption_algorithm if encryption_algorithm
600
- JWE.encrypt(token, jwe_key, **params)
601
- else
602
- token
603
- end
604
- end
605
-
606
- alias_method :jwt_encode_without_jwe, :jwt_encode
607
- alias_method :jwt_encode, :jwt_encode_with_jwe
608
- end
609
-
610
- def jwt_decode(
611
- token,
612
- jwks: nil,
613
- jws_algorithm: oauth_jwt_algorithm || oauth_jwt_public_key.keys.first || oauth_jwt_keys.keys.first,
614
- jws_key: oauth_jwt_public_key || oauth_jwt_keys[jws_algorithm] || _jwt_key,
615
- verify_claims: true,
616
- verify_jti: true,
617
- verify_iss: true,
618
- verify_aud: false
619
- )
620
- jws_key = jws_key.first if jws_key.is_a?(Array)
621
-
622
- # verifying the JWT implies verifying:
623
- #
624
- # issuer: check that server generated the token
625
- # aud: check the audience field (client is who he says he is)
626
- # iat: check that the token didn't expire
627
- #
628
- # subject can't be verified automatically without having access to the account id,
629
- # which we don't because that's the whole point.
630
- #
631
- verify_claims_params = if verify_claims
632
- {
633
- verify_iss: verify_iss,
634
- iss: issuer,
635
- # can't use stock aud verification, as it's dependent on the client application id
636
- verify_aud: false,
637
- verify_jti: (verify_jti ? method(:verify_jti) : false),
638
- verify_iat: true
639
- }
640
- else
641
- {}
642
- end
643
-
644
- # decode jwt
645
- claims = if is_authorization_server?
646
- if oauth_jwt_legacy_public_key
647
- algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
648
- JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
649
- elsif jwks
650
- JWT.decode(token, nil, true, algorithms: [jws_algorithm], jwks: { keys: jwks }, **verify_claims_params).first
651
- elsif jws_key
652
- JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
653
- end
654
- elsif (jwks = auth_server_jwks_set)
655
- algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
656
- JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
657
- end
658
-
659
- return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
660
-
661
- claims
662
- rescue JWT::DecodeError, JWT::JWKError
663
- nil
664
- end
665
-
666
- if defined?(JWE)
667
- def jwt_decode_with_jwe(
668
- token,
669
- jwks: nil,
670
- jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
671
- jws_encryption_method: oauth_jwt_jwe_encryption_method,
672
- jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_key,
673
- **args
674
- )
675
-
676
- token = if jwks && jwks.any? { |k| k[:use] == "enc" }
677
- JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
678
- elsif jwe_key
679
- jwe_key = jwe_key.first if jwe_key.is_a?(Array)
680
- JWE.decrypt(token, jwe_key)
681
- else
682
- token
683
- end
684
-
685
- jwt_decode_without_jwe(token, jwks: jwks, **args)
686
- rescue JWE::DecodeError => e
687
- jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
688
- end
689
-
690
- alias_method :jwt_decode_without_jwe, :jwt_decode
691
- alias_method :jwt_decode, :jwt_decode_with_jwe
692
- end
693
-
694
- def jwks_set
695
- @jwks_set ||= [
696
- *(
697
- unless oauth_jwt_public_keys.empty?
698
- oauth_jwt_public_keys.flat_map { |algo, pkeys| pkeys.map { |pkey| JWT::JWK.new(pkey).export.merge(use: "sig", alg: algo) } }
699
- end
700
- ),
701
- *(
702
- unless oauth_jwt_jwe_public_keys.empty?
703
- oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
704
- pkeys.map do |pkey|
705
- JWT::JWK.new(pkey).export.merge(use: "enc", alg: algo)
706
- end
707
- end
708
- end
709
- ),
710
- # legacy
711
- (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
712
- (
713
- if oauth_jwt_legacy_public_key
714
- JWT::JWK.new(oauth_jwt_legacy_public_key).export.merge(use: "sig", alg: oauth_jwt_legacy_algorithm)
715
- end
716
- ),
717
- (JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
718
- ].compact
719
- end
720
- else
721
- # :nocov:
722
- def jwk_import(_data)
723
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
724
- end
725
-
726
- def jwt_encode(_token)
727
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
728
- end
729
-
730
- def jwt_decode(_token, **)
731
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
732
- end
733
-
734
- def jwks_set
735
- raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
736
- end
737
- # :nocov:
738
- end
739
-
740
- def validate_oauth_revoke_params
741
- token_hint = param_or_nil("token_type_hint")
742
-
743
- throw(:rodauth_error) if !token_hint || token_hint == "access_token"
744
-
745
- super
746
- end
747
-
748
- def jwt_response_success(jwt, cache = false)
749
- response.status = 200
750
- response["Content-Type"] ||= "application/jwt"
751
- if cache
752
- # defaulting to 1-day for everyone, for now at least
753
- max_age = 60 * 60 * 24
754
- response["Cache-Control"] = "private, max-age=#{max_age}"
755
- else
756
- response["Cache-Control"] = "no-store"
757
- response["Pragma"] = "no-cache"
758
- end
759
- return_response(jwt)
760
- end
761
125
  end
762
126
  end