rodauth-oauth 0.7.3 → 0.9.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -418
- data/README.md +30 -390
- data/doc/release_notes/0_0_1.md +3 -0
- data/doc/release_notes/0_0_2.md +15 -0
- data/doc/release_notes/0_0_3.md +31 -0
- data/doc/release_notes/0_0_4.md +36 -0
- data/doc/release_notes/0_0_5.md +36 -0
- data/doc/release_notes/0_0_6.md +21 -0
- data/doc/release_notes/0_1_0.md +44 -0
- data/doc/release_notes/0_2_0.md +43 -0
- data/doc/release_notes/0_3_0.md +28 -0
- data/doc/release_notes/0_4_0.md +18 -0
- data/doc/release_notes/0_4_1.md +9 -0
- data/doc/release_notes/0_4_2.md +5 -0
- data/doc/release_notes/0_4_3.md +3 -0
- data/doc/release_notes/0_5_0.md +11 -0
- data/doc/release_notes/0_5_1.md +13 -0
- data/doc/release_notes/0_6_0.md +9 -0
- data/doc/release_notes/0_6_1.md +6 -0
- data/doc/release_notes/0_7_0.md +20 -0
- data/doc/release_notes/0_7_1.md +10 -0
- data/doc/release_notes/0_7_2.md +21 -0
- data/doc/release_notes/0_7_3.md +10 -0
- data/doc/release_notes/0_7_4.md +5 -0
- data/doc/release_notes/0_8_0.md +37 -0
- data/doc/release_notes/0_9_0.md +56 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +50 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +11 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +20 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +55 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +29 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +39 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +30 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +35 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +21 -1
- data/lib/rodauth/features/oauth.rb +3 -1418
- data/lib/rodauth/features/oauth_application_management.rb +225 -0
- data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +252 -0
- data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
- data/lib/rodauth/features/oauth_base.rb +771 -0
- data/lib/rodauth/features/oauth_client_credentials_grant.rb +33 -0
- data/lib/rodauth/features/oauth_device_grant.rb +220 -0
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +252 -0
- data/lib/rodauth/features/oauth_http_mac.rb +3 -21
- data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
- data/lib/rodauth/features/oauth_jwt.rb +276 -100
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
- data/lib/rodauth/features/oauth_management_base.rb +68 -0
- data/lib/rodauth/features/oauth_pkce.rb +98 -0
- data/lib/rodauth/features/oauth_resource_server.rb +21 -0
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
- data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
- data/lib/rodauth/features/oauth_token_management.rb +79 -0
- data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
- data/lib/rodauth/features/oidc.rb +36 -6
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +147 -0
- data/lib/rodauth/oauth/database_extensions.rb +15 -2
- data/lib/rodauth/oauth/jwe_extensions.rb +64 -0
- data/lib/rodauth/oauth/refinements.rb +48 -0
- data/lib/rodauth/oauth/ttl_store.rb +9 -3
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +33 -12
- data/templates/authorize.str +57 -8
- data/templates/client_secret_field.str +2 -2
- data/templates/description_field.str +1 -1
- data/templates/device_search.str +11 -0
- data/templates/device_verification.str +24 -0
- data/templates/homepage_url_field.str +2 -2
- data/templates/jwks_field.str +4 -0
- data/templates/jwt_public_key_field.str +4 -0
- data/templates/name_field.str +1 -1
- data/templates/new_oauth_application.str +9 -0
- data/templates/oauth_application.str +7 -3
- data/templates/oauth_application_oauth_tokens.str +52 -0
- data/templates/oauth_applications.str +3 -2
- data/templates/oauth_tokens.str +10 -11
- data/templates/redirect_uri_field.str +2 -2
- metadata +84 -4
- data/lib/rodauth/features/oauth_saml.rb +0 -104
@@ -10,16 +10,38 @@ module Rodauth
|
|
10
10
|
|
11
11
|
# Recommended to have hmac_secret as well
|
12
12
|
|
13
|
-
auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
|
13
|
+
auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
|
14
14
|
auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
|
15
15
|
|
16
16
|
auth_value_method :oauth_jwt_token_issuer, nil
|
17
17
|
|
18
|
-
|
18
|
+
configuration_module_eval do
|
19
|
+
define_method :oauth_applications_jws_jwk_column do
|
20
|
+
warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_column`"
|
21
|
+
oauth_applications_jwks_column
|
22
|
+
end
|
23
|
+
define_method :oauth_applications_jws_jwk_label do
|
24
|
+
warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_label`"
|
25
|
+
oauth_applications_jws_jwk_label
|
26
|
+
end
|
27
|
+
define_method :oauth_application_jws_jwk_param do
|
28
|
+
warn "#{__method__} is deprecated, switch to `oauth_applications_jwks_param`"
|
29
|
+
oauth_applications_jwks_param
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
auth_value_method :oauth_applications_subject_type_column, :subject_type
|
34
|
+
auth_value_method :oauth_applications_jwt_public_key_column, :jwt_public_key
|
35
|
+
auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
|
36
|
+
auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
|
37
|
+
auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
|
38
|
+
|
39
|
+
translatable_method :oauth_applications_jwt_public_key_label, "Public key"
|
19
40
|
|
41
|
+
auth_value_method :oauth_jwt_keys, {}
|
20
42
|
auth_value_method :oauth_jwt_key, nil
|
21
43
|
auth_value_method :oauth_jwt_public_key, nil
|
22
|
-
auth_value_method :oauth_jwt_algorithm, "
|
44
|
+
auth_value_method :oauth_jwt_algorithm, "RS256"
|
23
45
|
|
24
46
|
auth_value_method :oauth_jwt_jwe_key, nil
|
25
47
|
auth_value_method :oauth_jwt_jwe_public_key, nil
|
@@ -113,15 +135,19 @@ module Rodauth
|
|
113
135
|
|
114
136
|
return super unless request_object && oauth_application
|
115
137
|
|
116
|
-
|
117
|
-
|
138
|
+
if (jwks = oauth_application_jwks)
|
139
|
+
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
140
|
+
else
|
141
|
+
redirect_response_error("invalid_request_object")
|
142
|
+
end
|
118
143
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
144
|
+
request_sig_enc_opts = {
|
145
|
+
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
|
146
|
+
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
|
147
|
+
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
|
148
|
+
}.compact
|
123
149
|
|
124
|
-
claims = jwt_decode(request_object,
|
150
|
+
claims = jwt_decode(request_object, jwks: jwks, verify_jti: false, **request_sig_enc_opts)
|
125
151
|
|
126
152
|
redirect_response_error("invalid_request_object") unless claims
|
127
153
|
|
@@ -145,51 +171,6 @@ module Rodauth
|
|
145
171
|
|
146
172
|
# /token
|
147
173
|
|
148
|
-
def require_oauth_application
|
149
|
-
# requset authentication optional for assertions
|
150
|
-
return super unless param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
151
|
-
|
152
|
-
claims = jwt_decode(param("assertion"))
|
153
|
-
|
154
|
-
redirect_response_error("invalid_grant") unless claims
|
155
|
-
|
156
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
157
|
-
|
158
|
-
authorization_required unless @oauth_application
|
159
|
-
end
|
160
|
-
|
161
|
-
def validate_oauth_token_params
|
162
|
-
if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
163
|
-
redirect_response_error("invalid_client") unless param_or_nil("assertion")
|
164
|
-
else
|
165
|
-
super
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def create_oauth_token
|
170
|
-
if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
171
|
-
create_oauth_token_from_assertion
|
172
|
-
else
|
173
|
-
super
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def create_oauth_token_from_assertion
|
178
|
-
claims = jwt_decode(param("assertion"))
|
179
|
-
|
180
|
-
account = account_ds(claims["sub"]).first
|
181
|
-
|
182
|
-
redirect_response_error("invalid_client") unless oauth_application && account
|
183
|
-
|
184
|
-
create_params = {
|
185
|
-
oauth_tokens_account_id_column => claims["sub"],
|
186
|
-
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
187
|
-
oauth_tokens_scopes_column => claims["scope"]
|
188
|
-
}
|
189
|
-
|
190
|
-
generate_oauth_token(create_params, false)
|
191
|
-
end
|
192
|
-
|
193
174
|
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
194
175
|
create_params = {
|
195
176
|
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
@@ -249,7 +230,12 @@ module Rodauth
|
|
249
230
|
end
|
250
231
|
|
251
232
|
def jwt_subject(oauth_token)
|
252
|
-
|
233
|
+
subject_type = if oauth_application
|
234
|
+
oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
|
235
|
+
else
|
236
|
+
oauth_jwt_subject_type
|
237
|
+
end
|
238
|
+
case subject_type
|
253
239
|
when "public"
|
254
240
|
oauth_token[oauth_tokens_account_id_column]
|
255
241
|
when "pairwise"
|
@@ -257,7 +243,7 @@ module Rodauth
|
|
257
243
|
application_id = oauth_token[oauth_tokens_oauth_application_id_column]
|
258
244
|
Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
|
259
245
|
else
|
260
|
-
raise StandardError, "unexpected subject (#{
|
246
|
+
raise StandardError, "unexpected subject (#{subject_type})"
|
261
247
|
end
|
262
248
|
end
|
263
249
|
|
@@ -286,16 +272,36 @@ module Rodauth
|
|
286
272
|
}
|
287
273
|
end
|
288
274
|
|
289
|
-
def oauth_server_metadata_body(path)
|
275
|
+
def oauth_server_metadata_body(path = nil)
|
290
276
|
metadata = super
|
291
277
|
metadata.merge! \
|
292
278
|
jwks_uri: jwks_url,
|
293
|
-
token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
|
279
|
+
token_endpoint_auth_signing_alg_values_supported: (oauth_jwt_keys.keys + [oauth_jwt_algorithm]).uniq
|
294
280
|
metadata
|
295
281
|
end
|
296
282
|
|
297
283
|
def _jwt_key
|
298
|
-
@_jwt_key ||= oauth_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
|
299
305
|
end
|
300
306
|
|
301
307
|
# Resource Server only!
|
@@ -346,46 +352,125 @@ module Rodauth
|
|
346
352
|
generate_jti(claims) == jti
|
347
353
|
end
|
348
354
|
|
349
|
-
def verify_aud(
|
350
|
-
|
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
|
351
396
|
end
|
352
397
|
|
353
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
|
+
]
|
354
413
|
|
355
414
|
def jwk_import(data)
|
356
415
|
JSON::JWK.new(data)
|
357
416
|
end
|
358
417
|
|
359
|
-
|
360
|
-
|
418
|
+
def jwt_encode(payload,
|
419
|
+
jwks: nil,
|
420
|
+
jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
|
421
|
+
signing_algorithm: oauth_jwt_algorithm,
|
422
|
+
encryption_algorithm: oauth_jwt_jwe_algorithm,
|
423
|
+
encryption_method: oauth_jwt_jwe_encryption_method)
|
361
424
|
payload[:jti] = generate_jti(payload)
|
362
425
|
jwt = JSON::JWT.new(payload)
|
363
|
-
jwk = JSON::JWK.new(_jwt_key)
|
364
426
|
|
365
|
-
|
427
|
+
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
|
428
|
+
key = key.first if key.is_a?(Array)
|
429
|
+
|
430
|
+
jwk = JSON::JWK.new(key || "")
|
431
|
+
|
432
|
+
jwt = jwt.sign(jwk, signing_algorithm)
|
366
433
|
jwt.kid = jwk.thumbprint
|
367
434
|
|
368
|
-
if
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
435
|
+
if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
|
436
|
+
jwk = JSON::JWK.new(jwk)
|
437
|
+
jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
|
438
|
+
jwe.to_s
|
439
|
+
elsif jwe_key
|
440
|
+
algorithm = encryption_algorithm.to_sym if encryption_algorithm
|
441
|
+
meth = encryption_method.to_sym if encryption_method
|
442
|
+
jwt.encrypt(jwe_key, algorithm, meth)
|
443
|
+
else
|
444
|
+
jwt.to_s
|
373
445
|
end
|
374
|
-
jwt.to_s
|
375
446
|
end
|
376
447
|
|
377
448
|
def jwt_decode(
|
378
449
|
token,
|
450
|
+
jwks: nil,
|
379
451
|
jws_key: oauth_jwt_public_key || _jwt_key,
|
452
|
+
jws_algorithm: oauth_jwt_algorithm,
|
453
|
+
jwe_key: oauth_jwt_jwe_key,
|
454
|
+
jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
|
455
|
+
jws_encryption_method: oauth_jwt_jwe_encryption_method,
|
380
456
|
verify_claims: true,
|
381
457
|
verify_jti: true,
|
458
|
+
verify_iss: true,
|
459
|
+
verify_aud: false,
|
382
460
|
**
|
383
461
|
)
|
384
|
-
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if
|
462
|
+
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if jwe_key
|
385
463
|
|
386
464
|
claims = if is_authorization_server?
|
387
465
|
if oauth_jwt_legacy_public_key
|
388
466
|
JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
|
467
|
+
elsif jwks
|
468
|
+
enc_algs = [jws_encryption_algorithm].compact
|
469
|
+
enc_meths = [jws_encryption_method].compact
|
470
|
+
sig_algs = [jws_algorithm].compact.map(&:to_sym)
|
471
|
+
jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
|
472
|
+
jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
|
473
|
+
jws
|
389
474
|
elsif jws_key
|
390
475
|
JSON::JWT.decode(token, jws_key)
|
391
476
|
end
|
@@ -393,11 +478,15 @@ module Rodauth
|
|
393
478
|
JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
|
394
479
|
end
|
395
480
|
|
396
|
-
|
397
|
-
|
398
|
-
(!claims[:
|
399
|
-
(
|
400
|
-
(
|
481
|
+
now = Time.now
|
482
|
+
if verify_claims && (
|
483
|
+
(!claims[:exp] || Time.at(claims[:exp]) < now) &&
|
484
|
+
(claims[:nbf] && Time.at(claims[:nbf]) < now) &&
|
485
|
+
(claims[:iat] && Time.at(claims[:iat]) < now) &&
|
486
|
+
(verify_iss && claims[:iss] != issuer) &&
|
487
|
+
(verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
|
488
|
+
(verify_jti && !verify_jti(claims[:jti], claims))
|
489
|
+
)
|
401
490
|
return
|
402
491
|
end
|
403
492
|
|
@@ -415,20 +504,43 @@ module Rodauth
|
|
415
504
|
end
|
416
505
|
|
417
506
|
elsif defined?(JWT)
|
418
|
-
|
419
507
|
# ruby-jwt
|
508
|
+
require "rodauth/oauth/jwe_extensions" if defined?(JWE)
|
509
|
+
|
510
|
+
auth_value_method :oauth_jwt_algorithms_supported, %w[
|
511
|
+
HS256 HS384 HS512 HS512256
|
512
|
+
RS256 RS384 RS512
|
513
|
+
ED25519
|
514
|
+
ES256 ES384 ES512
|
515
|
+
PS256 PS384 PS512
|
516
|
+
]
|
517
|
+
|
518
|
+
auth_value_methods(
|
519
|
+
:oauth_jwt_jwe_algorithms_supported,
|
520
|
+
:oauth_jwt_jwe_encryption_methods_supported
|
521
|
+
)
|
522
|
+
|
523
|
+
def oauth_jwt_jwe_algorithms_supported
|
524
|
+
JWE::VALID_ALG
|
525
|
+
end
|
526
|
+
|
527
|
+
def oauth_jwt_jwe_encryption_methods_supported
|
528
|
+
JWE::VALID_ENC
|
529
|
+
end
|
420
530
|
|
421
531
|
def jwk_import(data)
|
422
532
|
JWT::JWK.import(data).keypair
|
423
533
|
end
|
424
534
|
|
425
|
-
def jwt_encode(payload)
|
535
|
+
def jwt_encode(payload, signing_algorithm: oauth_jwt_algorithm)
|
426
536
|
headers = {}
|
427
537
|
|
428
|
-
key = _jwt_key
|
538
|
+
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
|
539
|
+
key = key.first if key.is_a?(Array)
|
429
540
|
|
430
|
-
|
431
|
-
|
541
|
+
case key
|
542
|
+
when OpenSSL::PKey::PKey
|
543
|
+
jwk = JWT::JWK.new(key)
|
432
544
|
headers[:kid] = jwk.kid
|
433
545
|
|
434
546
|
key = jwk.keypair
|
@@ -436,31 +548,51 @@ module Rodauth
|
|
436
548
|
|
437
549
|
# @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
|
438
550
|
payload[:jti] = generate_jti(payload)
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
551
|
+
JWT.encode(payload, key, signing_algorithm, headers)
|
552
|
+
end
|
553
|
+
|
554
|
+
if defined?(JWE)
|
555
|
+
def jwt_encode_with_jwe(
|
556
|
+
payload,
|
557
|
+
jwks: nil,
|
558
|
+
jwe_key: oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
|
559
|
+
encryption_algorithm: oauth_jwt_jwe_algorithm,
|
560
|
+
encryption_method: oauth_jwt_jwe_encryption_method, **args
|
561
|
+
)
|
562
|
+
|
563
|
+
token = jwt_encode_without_jwe(payload, **args)
|
564
|
+
|
565
|
+
return token unless encryption_algorithm && encryption_method
|
566
|
+
|
567
|
+
if jwks && jwks.any? { |k| k[:use] == "enc" }
|
568
|
+
JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
|
569
|
+
elsif jwe_key
|
570
|
+
params = {
|
571
|
+
zip: "DEF",
|
572
|
+
copyright: oauth_jwt_jwe_copyright
|
573
|
+
}
|
574
|
+
params[:enc] = encryption_method if encryption_method
|
575
|
+
params[:alg] = encryption_algorithm if encryption_algorithm
|
576
|
+
JWE.encrypt(token, jwe_key, **params)
|
577
|
+
else
|
578
|
+
token
|
579
|
+
end
|
449
580
|
end
|
450
581
|
|
451
|
-
|
582
|
+
alias_method :jwt_encode_without_jwe, :jwt_encode
|
583
|
+
alias_method :jwt_encode, :jwt_encode_with_jwe
|
452
584
|
end
|
453
585
|
|
454
586
|
def jwt_decode(
|
455
587
|
token,
|
588
|
+
jwks: nil,
|
456
589
|
jws_key: oauth_jwt_public_key || _jwt_key,
|
457
590
|
jws_algorithm: oauth_jwt_algorithm,
|
458
591
|
verify_claims: true,
|
459
|
-
verify_jti: true
|
592
|
+
verify_jti: true,
|
593
|
+
verify_iss: true,
|
594
|
+
verify_aud: false
|
460
595
|
)
|
461
|
-
# decrypt jwe
|
462
|
-
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
|
463
|
-
|
464
596
|
# verifying the JWT implies verifying:
|
465
597
|
#
|
466
598
|
# issuer: check that server generated the token
|
@@ -472,7 +604,7 @@ module Rodauth
|
|
472
604
|
#
|
473
605
|
verify_claims_params = if verify_claims
|
474
606
|
{
|
475
|
-
verify_iss:
|
607
|
+
verify_iss: verify_iss,
|
476
608
|
iss: issuer,
|
477
609
|
# can't use stock aud verification, as it's dependent on the client application id
|
478
610
|
verify_aud: false,
|
@@ -488,6 +620,8 @@ module Rodauth
|
|
488
620
|
if oauth_jwt_legacy_public_key
|
489
621
|
algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
490
622
|
JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
|
623
|
+
elsif jwks
|
624
|
+
JWT.decode(token, nil, true, algorithms: [jws_algorithm], jwks: { keys: jwks }, **verify_claims_params).first
|
491
625
|
elsif jws_key
|
492
626
|
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
|
493
627
|
end
|
@@ -496,13 +630,40 @@ module Rodauth
|
|
496
630
|
JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
|
497
631
|
end
|
498
632
|
|
499
|
-
return if verify_claims && !verify_aud(claims["aud"], claims)
|
633
|
+
return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
|
500
634
|
|
501
635
|
claims
|
502
636
|
rescue JWT::DecodeError, JWT::JWKError
|
503
637
|
nil
|
504
638
|
end
|
505
639
|
|
640
|
+
if defined?(JWE)
|
641
|
+
def jwt_decode_with_jwe(
|
642
|
+
token,
|
643
|
+
jwks: nil,
|
644
|
+
jwe_key: oauth_jwt_jwe_key,
|
645
|
+
jws_encryption_algorithm: oauth_jwt_jwe_algorithm,
|
646
|
+
jws_encryption_method: oauth_jwt_jwe_encryption_method,
|
647
|
+
**args
|
648
|
+
)
|
649
|
+
|
650
|
+
token = if jwks && jwks.any? { |k| k[:use] == "enc" }
|
651
|
+
JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
|
652
|
+
elsif jwe_key
|
653
|
+
JWE.decrypt(token, jwe_key)
|
654
|
+
else
|
655
|
+
token
|
656
|
+
end
|
657
|
+
|
658
|
+
jwt_decode_without_jwe(token, jwks: jwks, **args)
|
659
|
+
rescue JWE::DecodeError => e
|
660
|
+
jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
|
661
|
+
end
|
662
|
+
|
663
|
+
alias_method :jwt_decode_without_jwe, :jwt_decode
|
664
|
+
alias_method :jwt_decode, :jwt_decode_with_jwe
|
665
|
+
end
|
666
|
+
|
506
667
|
def jwks_set
|
507
668
|
@jwks_set ||= [
|
508
669
|
(JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
@@ -541,5 +702,20 @@ module Rodauth
|
|
541
702
|
|
542
703
|
super
|
543
704
|
end
|
705
|
+
|
706
|
+
def jwt_response_success(jwt, cache = false)
|
707
|
+
response.status = 200
|
708
|
+
response["Content-Type"] ||= "application/jwt"
|
709
|
+
if cache
|
710
|
+
# defaulting to 1-day for everyone, for now at least
|
711
|
+
max_age = 60 * 60 * 24
|
712
|
+
response["Cache-Control"] = "private, max-age=#{max_age}"
|
713
|
+
else
|
714
|
+
response["Cache-Control"] = "no-store"
|
715
|
+
response["Pragma"] = "no-cache"
|
716
|
+
end
|
717
|
+
response.write(jwt)
|
718
|
+
request.halt
|
719
|
+
end
|
544
720
|
end
|
545
721
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth/ttl_store"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_jwt_bearer_grant, :OauthJwtBearerGrant) do
|
7
|
+
depends :oauth_assertion_base, :oauth_jwt
|
8
|
+
|
9
|
+
auth_value_methods(
|
10
|
+
:require_oauth_application_from_jwt_bearer_assertion_issuer,
|
11
|
+
:require_oauth_application_from_jwt_bearer_assertion_subject,
|
12
|
+
:account_from_jwt_bearer_assertion
|
13
|
+
)
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
|
18
|
+
claims = jwt_assertion(assertion)
|
19
|
+
|
20
|
+
return unless claims
|
21
|
+
|
22
|
+
db[oauth_applications_table].where(
|
23
|
+
oauth_applications_client_id_column => claims["iss"]
|
24
|
+
).first
|
25
|
+
end
|
26
|
+
|
27
|
+
def require_oauth_application_from_jwt_bearer_assertion_subject(assertion)
|
28
|
+
claims = jwt_assertion(assertion)
|
29
|
+
|
30
|
+
return unless claims
|
31
|
+
|
32
|
+
db[oauth_applications_table].where(
|
33
|
+
oauth_applications_client_id_column => claims["sub"]
|
34
|
+
).first
|
35
|
+
end
|
36
|
+
|
37
|
+
def account_from_jwt_bearer_assertion(assertion)
|
38
|
+
claims = jwt_assertion(assertion)
|
39
|
+
|
40
|
+
return unless claims
|
41
|
+
|
42
|
+
account_from_bearer_assertion_subject(claims["sub"])
|
43
|
+
end
|
44
|
+
|
45
|
+
def jwt_assertion(assertion)
|
46
|
+
claims = jwt_decode(assertion, verify_iss: false, verify_aud: false)
|
47
|
+
return unless verify_aud(token_url, claims["aud"])
|
48
|
+
|
49
|
+
claims
|
50
|
+
end
|
51
|
+
|
52
|
+
def oauth_server_metadata_body(*)
|
53
|
+
super.tap do |data|
|
54
|
+
data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
55
|
+
data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_management_base, :OauthManagementBase) do
|
5
|
+
depends :oauth_base
|
6
|
+
|
7
|
+
button "Previous", "oauth_management_pagination_previous"
|
8
|
+
button "Next", "oauth_management_pagination_next"
|
9
|
+
|
10
|
+
def oauth_management_pagination_links(paginated_ds)
|
11
|
+
html = +'<nav aria-label="Pagination"><ul class="pagination">'
|
12
|
+
html << oauth_management_pagination_link(paginated_ds.prev_page, label: oauth_management_pagination_previous_button)
|
13
|
+
html << oauth_management_pagination_link(paginated_ds.current_page - 1) unless paginated_ds.first_page?
|
14
|
+
html << oauth_management_pagination_link(paginated_ds.current_page, label: paginated_ds.current_page, current: true)
|
15
|
+
html << oauth_management_pagination_link(paginated_ds.current_page + 1) unless paginated_ds.last_page?
|
16
|
+
html << oauth_management_pagination_link(paginated_ds.next_page, label: oauth_management_pagination_next_button)
|
17
|
+
html << "</ul></nav>"
|
18
|
+
end
|
19
|
+
|
20
|
+
def oauth_management_pagination_link(page, label: page, current: false, classes: "")
|
21
|
+
classes += " disabled" if current || !page
|
22
|
+
classes += " active" if current
|
23
|
+
if page
|
24
|
+
params = request.GET.merge("page" => page).map do |k, v|
|
25
|
+
v ? "#{CGI.escape(String(k))}=#{CGI.escape(String(v))}" : CGI.escape(String(k))
|
26
|
+
end.join("&")
|
27
|
+
|
28
|
+
href = "#{request.path}?#{params}"
|
29
|
+
|
30
|
+
<<-HTML
|
31
|
+
<li class="page-item #{classes}" #{'aria-current="page"' if current}>
|
32
|
+
<a class="page-link" href="#{href}" tabindex="-1" aria-disabled="#{current || !page}">
|
33
|
+
#{label}
|
34
|
+
</a>
|
35
|
+
</li>
|
36
|
+
HTML
|
37
|
+
else
|
38
|
+
<<-HTML
|
39
|
+
<li class="page-item #{classes}">
|
40
|
+
<span class="page-link">
|
41
|
+
#{label}
|
42
|
+
#{'<span class="sr-only">(current)</span>' if current}
|
43
|
+
</span>
|
44
|
+
</li>
|
45
|
+
HTML
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def post_configure
|
50
|
+
super
|
51
|
+
db.extension :pagination
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def per_page_param(default_per_page)
|
57
|
+
per_page = param_or_nil("per_page")
|
58
|
+
|
59
|
+
return default_per_page unless per_page
|
60
|
+
|
61
|
+
per_page = per_page.to_i
|
62
|
+
|
63
|
+
return default_per_page if per_page <= 0
|
64
|
+
|
65
|
+
[per_page, default_per_page].min
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|