rodauth-oauth 0.10.4 → 1.0.0.pre.beta2

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 +28 -35
  4. data/doc/release_notes/1_0_0_beta1.md +38 -0
  5. data/doc/release_notes/1_0_0_beta2.md +34 -0
  6. data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +21 -11
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
  13. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
  14. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
  15. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +57 -57
  16. data/lib/rodauth/features/oauth_application_management.rb +61 -74
  17. data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
  18. data/lib/rodauth/features/oauth_authorization_code_grant.rb +62 -90
  19. data/lib/rodauth/features/oauth_authorize_base.rb +115 -22
  20. data/lib/rodauth/features/oauth_base.rb +397 -315
  21. data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
  22. data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
  23. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +52 -31
  24. data/lib/rodauth/features/oauth_grant_management.rb +70 -0
  25. data/lib/rodauth/features/oauth_implicit_grant.rb +29 -27
  26. data/lib/rodauth/features/oauth_jwt.rb +53 -689
  27. data/lib/rodauth/features/oauth_jwt_base.rb +458 -0
  28. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +48 -17
  29. data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
  30. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +116 -0
  31. data/lib/rodauth/features/oauth_management_base.rb +2 -0
  32. data/lib/rodauth/features/oauth_pkce.rb +22 -26
  33. data/lib/rodauth/features/oauth_resource_indicators.rb +33 -25
  34. data/lib/rodauth/features/oauth_resource_server.rb +59 -0
  35. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +7 -1
  36. data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
  37. data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
  38. data/lib/rodauth/features/oidc.rb +382 -241
  39. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +127 -51
  40. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +115 -0
  41. data/lib/rodauth/oauth/database_extensions.rb +8 -6
  42. data/lib/rodauth/oauth/http_extensions.rb +74 -0
  43. data/lib/rodauth/oauth/railtie.rb +20 -0
  44. data/lib/rodauth/oauth/ttl_store.rb +2 -0
  45. data/lib/rodauth/oauth/version.rb +1 -1
  46. data/lib/rodauth/oauth.rb +29 -1
  47. data/locales/en.yml +34 -22
  48. data/locales/pt.yml +34 -22
  49. data/templates/authorize.str +19 -17
  50. data/templates/device_search.str +1 -1
  51. data/templates/device_verification.str +2 -2
  52. data/templates/jwks_field.str +1 -0
  53. data/templates/new_oauth_application.str +1 -2
  54. data/templates/oauth_application.str +2 -2
  55. data/templates/oauth_application_oauth_grants.str +54 -0
  56. data/templates/oauth_applications.str +2 -2
  57. data/templates/oauth_grants.str +52 -0
  58. metadata +23 -16
  59. data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
  60. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
  61. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
  62. data/lib/rodauth/features/oauth.rb +0 -9
  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,116 +20,91 @@ 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
100
-
101
- def issuer
102
- @issuer ||= oauth_jwt_token_issuer || authorization_server_url
103
- end
104
-
105
- def authorization_token
106
- return @authorization_token if defined?(@authorization_token)
107
-
108
- @authorization_token = begin
109
- bearer_token = fetch_access_token
110
-
111
- return unless bearer_token
30
+ def current_oauth_account
31
+ subject = oauth_token_subject
112
32
 
113
- jwt_token = jwt_decode(bearer_token)
33
+ return if subject == authorization_token["client_id"]
114
34
 
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
35
+ oauth_account_ds(subject).first
123
36
  end
124
37
 
125
- # /authorize
38
+ def current_oauth_application
39
+ db[oauth_applications_table].where(
40
+ oauth_applications_client_id_column => authorization_token["client_id"]
41
+ ).first
42
+ end
126
43
 
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")
44
+ private
130
45
 
131
- request_object = param_or_nil("request")
46
+ def authorization_token
47
+ return super unless oauth_jwt_access_tokens
132
48
 
133
- return super unless request_object && oauth_application
49
+ return @authorization_token if defined?(@authorization_token)
134
50
 
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")
139
- end
51
+ @authorization_token = begin
52
+ access_token = fetch_access_token
140
53
 
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
54
+ return unless access_token
146
55
 
147
- claims = jwt_decode(request_object, jwks: jwks, verify_jti: false, **request_sig_enc_opts)
56
+ jwt_claims = jwt_decode(access_token)
148
57
 
149
- redirect_response_error("invalid_request_object") unless claims
58
+ return unless jwt_claims
150
59
 
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")
60
+ return unless jwt_claims["sub"]
159
61
 
160
- redirect_response_error("invalid_request_object") if audience && audience != issuer
62
+ return unless jwt_claims["aud"]
161
63
 
162
- claims.each do |k, v|
163
- request.params[k.to_s] = v
64
+ jwt_claims
164
65
  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])
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 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"]
121
+ exp: issued_at + oauth_access_token_expires_in,
122
+ aud: oauth_jwt_audience
272
123
  }
273
124
  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