rodauth-oauth 0.7.4 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -424
- 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/doc/release_notes/0_9_1.md +9 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +25 -4
- 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 +27 -10
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +17 -5
- 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 +6 -5
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +12 -15
- 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 +778 -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 +275 -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 +38 -9
- 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 +80 -3
- 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,19 @@ 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
|
+
return_response(jwt)
|
718
|
+
end
|
544
719
|
end
|
545
720
|
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
|