rodauth-oauth 1.3.1 → 1.4.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/README.md +19 -12
- data/doc/release_notes/1_3_2.md +14 -0
- data/doc/release_notes/1_4_0.md +49 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +23 -23
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/frontchannel_logout.html.erb +10 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +1 -1
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +21 -0
- data/lib/generators/rodauth/oauth/views_generator.rb +2 -2
- data/lib/rodauth/features/oauth_application_management.rb +1 -1
- data/lib/rodauth/features/oauth_assertion_base.rb +1 -1
- data/lib/rodauth/features/oauth_authorize_base.rb +1 -1
- data/lib/rodauth/features/oauth_base.rb +31 -30
- data/lib/rodauth/features/oauth_device_code_grant.rb +2 -2
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +9 -0
- data/lib/rodauth/features/oauth_grant_management.rb +1 -1
- data/lib/rodauth/features/oauth_jwt.rb +3 -3
- data/lib/rodauth/features/oauth_jwt_base.rb +23 -12
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +1 -1
- data/lib/rodauth/features/oauth_jwt_jwks.rb +1 -1
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +25 -6
- data/lib/rodauth/features/oauth_pushed_authorization_request.rb +33 -24
- data/lib/rodauth/features/oauth_resource_server.rb +1 -1
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +79 -47
- data/lib/rodauth/features/oauth_tls_client_auth.rb +1 -1
- data/lib/rodauth/features/oauth_token_introspection.rb +1 -1
- data/lib/rodauth/features/oauth_token_revocation.rb +1 -1
- data/lib/rodauth/features/oidc.rb +27 -8
- data/lib/rodauth/features/oidc_backchannel_logout.rb +120 -0
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +25 -0
- data/lib/rodauth/features/oidc_frontchannel_logout.rb +134 -0
- data/lib/rodauth/features/oidc_logout_base.rb +76 -0
- data/lib/rodauth/features/oidc_rp_initiated_logout.rb +29 -6
- data/lib/rodauth/features/oidc_session_management.rb +89 -0
- data/lib/rodauth/oauth/http_extensions.rb +1 -1
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +9 -0
- data/locales/pt.yml +9 -0
- data/templates/check_session.str +67 -0
- data/templates/frontchannel_logout.str +17 -0
- metadata +13 -2
@@ -9,13 +9,15 @@ module Rodauth
|
|
9
9
|
depends :oauth_authorize_base, :oauth_jwt_base
|
10
10
|
|
11
11
|
auth_value_method :oauth_require_request_uri_registration, false
|
12
|
+
auth_value_method :oauth_require_signed_request_object, false
|
12
13
|
auth_value_method :oauth_request_object_signing_alg_allow_none, false
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
%i[
|
16
|
+
request_uris require_signed_request_object request_object_signing_alg
|
17
|
+
request_object_encryption_alg request_object_encryption_enc
|
18
|
+
].each do |column|
|
19
|
+
auth_value_method :"oauth_applications_#{column}_column", column
|
20
|
+
end
|
19
21
|
|
20
22
|
translatable_method :oauth_invalid_request_object_message, "request object is invalid"
|
21
23
|
|
@@ -30,7 +32,13 @@ module Rodauth
|
|
30
32
|
|
31
33
|
request_uri = param_or_nil("request_uri")
|
32
34
|
|
33
|
-
|
35
|
+
unless (request_object || request_uri) && oauth_application
|
36
|
+
if request.path == authorize_path && request.get? && require_signed_request_object?
|
37
|
+
redirect_response_error("invalid_request_object")
|
38
|
+
end
|
39
|
+
|
40
|
+
return super
|
41
|
+
end
|
34
42
|
|
35
43
|
if request_uri
|
36
44
|
request_uri = CGI.unescape(request_uri)
|
@@ -84,6 +92,14 @@ module Rodauth
|
|
84
92
|
request_uris.nil? || request_uris.split(oauth_scope_separator).one? { |uri| request_uri.start_with?(uri) }
|
85
93
|
end
|
86
94
|
|
95
|
+
def require_signed_request_object?
|
96
|
+
return @require_signed_request_object if defined?(@require_signed_request_object)
|
97
|
+
|
98
|
+
@require_signed_request_object = (oauth_application[oauth_applications_require_signed_request_object_column] if oauth_application)
|
99
|
+
@require_signed_request_object = oauth_require_signed_request_object if @require_signed_request_object.nil?
|
100
|
+
@require_signed_request_object
|
101
|
+
end
|
102
|
+
|
87
103
|
def decode_request_object(request_object)
|
88
104
|
request_sig_enc_opts = {
|
89
105
|
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
|
@@ -94,6 +110,8 @@ module Rodauth
|
|
94
110
|
request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
|
95
111
|
|
96
112
|
if request_sig_enc_opts[:jws_algorithm] == "none"
|
113
|
+
redirect_response_error("invalid_request_object") if require_signed_request_object?
|
114
|
+
|
97
115
|
jwks = nil
|
98
116
|
elsif (jwks = oauth_application_jwks(oauth_application))
|
99
117
|
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
@@ -118,6 +136,7 @@ module Rodauth
|
|
118
136
|
data[:request_parameter_supported] = true
|
119
137
|
data[:request_uri_parameter_supported] = true
|
120
138
|
data[:require_request_uri_registration] = oauth_require_request_uri_registration
|
139
|
+
data[:require_signed_request_object] = oauth_require_signed_request_object
|
121
140
|
end
|
122
141
|
end
|
123
142
|
end
|
@@ -65,32 +65,37 @@ module Rodauth
|
|
65
65
|
# The request_uri authorization request parameter is one exception, and it MUST NOT be provided.
|
66
66
|
redirect_response_error("invalid_request") if param_or_nil("request_uri")
|
67
67
|
|
68
|
-
if
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
68
|
+
if features.include?(:oauth_jwt_secured_authorization_request)
|
69
|
+
|
70
|
+
if (request_object = param_or_nil("request"))
|
71
|
+
claims = decode_request_object(request_object)
|
72
|
+
|
73
|
+
# https://datatracker.ietf.org/doc/html/rfc9126#section-3-5.3
|
74
|
+
# reject the request if the authenticated client_id does not match the client_id claim in the Request Object
|
75
|
+
if (client_id = claims["client_id"]) && (client_id != oauth_application[oauth_applications_client_id_column])
|
76
|
+
redirect_response_error("invalid_request_object")
|
77
|
+
end
|
78
|
+
|
79
|
+
# requiring the iss claim to match the client_id is at the discretion of the authorization server
|
80
|
+
if oauth_require_pushed_authorization_request_iss_request_object &&
|
81
|
+
(iss = claims.delete("iss")) &&
|
82
|
+
iss != oauth_application[oauth_applications_client_id_column]
|
83
|
+
redirect_response_error("invalid_request_object")
|
84
|
+
end
|
85
|
+
|
86
|
+
if (aud = claims.delete("aud")) && !verify_aud(aud, oauth_jwt_issuer)
|
87
|
+
redirect_response_error("invalid_request_object")
|
88
|
+
end
|
89
|
+
|
90
|
+
claims.delete("exp")
|
91
|
+
request.params.delete("request")
|
92
|
+
|
93
|
+
claims.each do |k, v|
|
94
|
+
request.params[k.to_s] = v
|
95
|
+
end
|
96
|
+
elsif require_signed_request_object?
|
85
97
|
redirect_response_error("invalid_request_object")
|
86
98
|
end
|
87
|
-
|
88
|
-
claims.delete("exp")
|
89
|
-
request.params.delete("request")
|
90
|
-
|
91
|
-
claims.each do |k, v|
|
92
|
-
request.params[k.to_s] = v
|
93
|
-
end
|
94
99
|
end
|
95
100
|
|
96
101
|
validate_authorize_params
|
@@ -118,6 +123,10 @@ module Rodauth
|
|
118
123
|
request.params[k.to_s] = v
|
119
124
|
end
|
120
125
|
|
126
|
+
request.params.delete("request_uri")
|
127
|
+
|
128
|
+
# we're removing the request_uri here, so the checkup for signed reqest has to be invalidated.
|
129
|
+
@require_signed_request_object = false
|
121
130
|
elsif oauth_require_pushed_authorization_requests ||
|
122
131
|
(oauth_application && oauth_application[oauth_applications_require_pushed_authorization_requests_column])
|
123
132
|
redirect_authorize_error("request_uri")
|
@@ -7,19 +7,28 @@ module Rodauth
|
|
7
7
|
Feature.define(:oauth_saml_bearer_grant, :OauthSamlBearerGrant) do
|
8
8
|
depends :oauth_assertion_base
|
9
9
|
|
10
|
-
auth_value_method :oauth_saml_cert_fingerprint, "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
|
11
|
-
auth_value_method :oauth_saml_cert, nil
|
12
|
-
auth_value_method :oauth_saml_cert_fingerprint_algorithm, nil
|
13
10
|
auth_value_method :oauth_saml_name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
14
|
-
|
15
|
-
auth_value_method :oauth_saml_security_authn_requests_signed, true
|
16
|
-
auth_value_method :oauth_saml_security_metadata_signed, true
|
17
|
-
auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
|
18
|
-
auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
|
11
|
+
auth_value_method :oauth_saml_idp_cert_check_expiration, true
|
19
12
|
|
20
13
|
auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
|
21
14
|
|
22
|
-
|
15
|
+
auth_value_method :oauth_saml_settings_table, :oauth_saml_settings
|
16
|
+
%i[
|
17
|
+
id oauth_application_id
|
18
|
+
idp_cert idp_cert_fingerprint idp_cert_fingerprint_algorithm
|
19
|
+
name_identifier_format
|
20
|
+
issuer
|
21
|
+
audience
|
22
|
+
idp_cert_check_expiration
|
23
|
+
].each do |column|
|
24
|
+
auth_value_method :"oauth_saml_settings_#{column}_column", column
|
25
|
+
end
|
26
|
+
|
27
|
+
translatable_method :oauth_saml_assertion_not_base64_message, "SAML assertion must be in base64 format"
|
28
|
+
translatable_method :oauth_saml_assertion_single_issuer_message, "SAML assertion must have a single issuer"
|
29
|
+
translatable_method :oauth_saml_settings_not_found_message, "No SAML settings found for issuer"
|
30
|
+
|
31
|
+
auth_methods(
|
23
32
|
:require_oauth_application_from_saml2_bearer_assertion_issuer,
|
24
33
|
:require_oauth_application_from_saml2_bearer_assertion_subject,
|
25
34
|
:account_from_saml2_bearer_assertion
|
@@ -32,72 +41,95 @@ module Rodauth
|
|
32
41
|
private
|
33
42
|
|
34
43
|
def require_oauth_application_from_saml2_bearer_assertion_issuer(assertion)
|
35
|
-
|
44
|
+
parse_saml_assertion(assertion)
|
36
45
|
|
37
|
-
return unless
|
46
|
+
return unless @saml_settings
|
38
47
|
|
39
48
|
db[oauth_applications_table].where(
|
40
|
-
|
49
|
+
oauth_applications_id_column => @saml_settings[oauth_saml_settings_oauth_application_id_column]
|
41
50
|
).first
|
42
51
|
end
|
43
52
|
|
44
53
|
def require_oauth_application_from_saml2_bearer_assertion_subject(assertion)
|
45
|
-
|
54
|
+
parse_saml_assertion(assertion)
|
46
55
|
|
47
|
-
return unless
|
56
|
+
return unless @assertion
|
48
57
|
|
58
|
+
# 3.3.8 - For client authentication, the Subject MUST be the "client_id" of the OAuth client.
|
49
59
|
db[oauth_applications_table].where(
|
50
|
-
oauth_applications_client_id_column =>
|
60
|
+
oauth_applications_client_id_column => @assertion.nameid
|
51
61
|
).first
|
52
62
|
end
|
53
63
|
|
54
64
|
def account_from_saml2_bearer_assertion(assertion)
|
55
|
-
|
65
|
+
parse_saml_assertion(assertion)
|
56
66
|
|
57
|
-
return unless
|
67
|
+
return unless @assertion
|
58
68
|
|
59
|
-
account_from_bearer_assertion_subject(
|
69
|
+
account_from_bearer_assertion_subject(@assertion.nameid)
|
60
70
|
end
|
61
71
|
|
62
|
-
def
|
72
|
+
def generate_saml_settings(saml_settings)
|
63
73
|
settings = OneLogin::RubySaml::Settings.new
|
64
|
-
settings.idp_cert = oauth_saml_cert
|
65
|
-
settings.idp_cert_fingerprint = oauth_saml_cert_fingerprint
|
66
|
-
settings.idp_cert_fingerprint_algorithm = oauth_saml_cert_fingerprint_algorithm
|
67
|
-
settings.name_identifier_format = oauth_saml_name_identifier_format
|
68
|
-
settings.security[:authn_requests_signed] = oauth_saml_security_authn_requests_signed
|
69
|
-
settings.security[:metadata_signed] = oauth_saml_security_metadata_signed
|
70
|
-
settings.security[:digest_method] = oauth_saml_security_digest_method
|
71
|
-
settings.security[:signature_method] = oauth_saml_security_signature_method
|
72
74
|
|
73
|
-
|
75
|
+
# issuer
|
76
|
+
settings.idp_entity_id = saml_settings[oauth_saml_settings_issuer_column]
|
74
77
|
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
#
|
79
|
-
|
80
|
-
|
78
|
+
# audience
|
79
|
+
settings.sp_entity_id = saml_settings[oauth_saml_settings_audience_column] || token_url
|
80
|
+
|
81
|
+
# recipient
|
82
|
+
settings.assertion_consumer_service_url = token_url
|
83
|
+
|
84
|
+
settings.idp_cert = saml_settings[oauth_saml_settings_idp_cert_column]
|
85
|
+
settings.idp_cert_fingerprint = saml_settings[oauth_saml_settings_idp_cert_fingerprint_column]
|
86
|
+
settings.idp_cert_fingerprint_algorithm = saml_settings[oauth_saml_settings_idp_cert_fingerprint_algorithm_column]
|
87
|
+
|
88
|
+
if settings.idp_cert
|
89
|
+
check_idp_cert_expiration = saml_settings[oauth_saml_settings_idp_cert_check_expiration_column]
|
90
|
+
check_idp_cert_expiration = oauth_saml_idp_cert_check_expiration if check_idp_cert_expiration.nil?
|
91
|
+
settings.security[:check_idp_cert_expiration] = check_idp_cert_expiration
|
92
|
+
end
|
93
|
+
settings.security[:strict_audience_validation] = true
|
94
|
+
settings.security[:want_name_id] = true
|
81
95
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
96
|
+
settings.name_identifier_format = saml_settings[oauth_saml_settings_name_identifier_format_column] ||
|
97
|
+
oauth_saml_name_identifier_format
|
98
|
+
settings
|
99
|
+
end
|
100
|
+
|
101
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName
|
102
|
+
def parse_saml_assertion(assertion)
|
103
|
+
return @assertion if defined?(@assertion)
|
104
|
+
|
105
|
+
response = OneLogin::RubySaml::Response.new(assertion)
|
106
|
+
|
107
|
+
# The SAML Assertion XML data MUST be encoded using base64url
|
108
|
+
redirect_response_error("invalid_grant", oauth_saml_assertion_not_base64_message) unless response.send(:base64_encoded?, assertion)
|
86
109
|
|
87
110
|
# 1. The Assertion's <Issuer> element MUST contain a unique identifier
|
88
111
|
# for the entity that issued the Assertion.
|
89
|
-
redirect_response_error("invalid_grant") unless response.issuers.size == 1
|
112
|
+
redirect_response_error("invalid_grant", oauth_saml_assertion_single_issuer_message) unless response.issuers.size == 1
|
113
|
+
|
114
|
+
@saml_settings = db[oauth_saml_settings_table].where(
|
115
|
+
oauth_saml_settings_issuer_column => response.issuers.first
|
116
|
+
).first
|
117
|
+
|
118
|
+
redirect_response_error("invalid_grant", oauth_saml_settings_not_found_message) unless @saml_settings
|
90
119
|
|
91
|
-
|
92
|
-
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
|
120
|
+
response.settings = generate_saml_settings(@saml_settings)
|
121
|
+
|
122
|
+
# 2. The Assertion MUST contain a <Conditions> element ...
|
123
|
+
# 3. he Assertion MUST have an expiry that limits the time window ...
|
124
|
+
# 4. The Assertion MUST have an expiry that limits the time window ...
|
125
|
+
# 5. The <Subject> element MUST contain at least one ...
|
126
|
+
# 6. The authorization server MUST reject the entire Assertion if the ...
|
127
|
+
# 7. If the Assertion issuer directly authenticated the subject, ...
|
128
|
+
redirect_response_error("invalid_grant", response.errors.join("; ")) unless response.is_valid?
|
98
129
|
|
99
|
-
response
|
130
|
+
@assertion = response
|
100
131
|
end
|
132
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
101
133
|
|
102
134
|
def oauth_server_metadata_body(*)
|
103
135
|
super.tap do |data|
|
@@ -51,6 +51,11 @@ module Rodauth
|
|
51
51
|
require_request_uri_registration
|
52
52
|
op_policy_uri
|
53
53
|
op_tos_uri
|
54
|
+
check_session_iframe
|
55
|
+
frontchannel_logout_supported
|
56
|
+
frontchannel_logout_session_supported
|
57
|
+
backchannel_logout_supported
|
58
|
+
backchannel_logout_session_supported
|
54
59
|
].freeze
|
55
60
|
|
56
61
|
REQUIRED_METADATA_KEYS = %i[
|
@@ -63,7 +68,7 @@ module Rodauth
|
|
63
68
|
id_token_signing_alg_values_supported
|
64
69
|
].freeze
|
65
70
|
|
66
|
-
depends :
|
71
|
+
depends :active_sessions, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant, :oauth_implicit_grant
|
67
72
|
|
68
73
|
auth_value_method :oauth_application_scopes, %w[openid]
|
69
74
|
|
@@ -95,7 +100,10 @@ module Rodauth
|
|
95
100
|
:request_object_signing_alg_values_supported,
|
96
101
|
:request_object_encryption_alg_values_supported,
|
97
102
|
:request_object_encryption_enc_values_supported,
|
98
|
-
:oauth_acr_values_supported
|
103
|
+
:oauth_acr_values_supported
|
104
|
+
)
|
105
|
+
|
106
|
+
auth_methods(
|
99
107
|
:get_oidc_account_last_login_at,
|
100
108
|
:oidc_authorize_on_prompt_none?,
|
101
109
|
:fill_with_account_claims,
|
@@ -222,7 +230,7 @@ module Rodauth
|
|
222
230
|
def current_oauth_account
|
223
231
|
subject_type = current_oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
|
224
232
|
|
225
|
-
|
233
|
+
super unless subject_type == "pairwise"
|
226
234
|
end
|
227
235
|
|
228
236
|
private
|
@@ -341,10 +349,17 @@ module Rodauth
|
|
341
349
|
end
|
342
350
|
|
343
351
|
def get_oidc_account_last_login_at(account_id)
|
344
|
-
get_activity_timestamp(account_id, account_activity_last_activity_column)
|
352
|
+
return get_activity_timestamp(account_id, account_activity_last_activity_column) if features.include?(:account_expiration)
|
353
|
+
|
354
|
+
# active sessions based
|
355
|
+
ds = db[active_sessions_table].where(active_sessions_account_id_column => account_id)
|
356
|
+
|
357
|
+
ds = ds.order(Sequel.desc(active_sessions_created_at_column))
|
358
|
+
|
359
|
+
convert_timestamp(ds.get(active_sessions_created_at_column))
|
345
360
|
end
|
346
361
|
|
347
|
-
def jwt_subject(
|
362
|
+
def jwt_subject(account_unique_id, client_application = oauth_application)
|
348
363
|
subject_type = client_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
|
349
364
|
|
350
365
|
case subject_type
|
@@ -368,8 +383,7 @@ module Rodauth
|
|
368
383
|
|
369
384
|
identifier_uri = URI(identifier_uri).host
|
370
385
|
|
371
|
-
|
372
|
-
values = [identifier_uri, *account_ids, oauth_jwt_subject_secret]
|
386
|
+
values = [identifier_uri, account_unique_id, oauth_jwt_subject_secret]
|
373
387
|
Digest::SHA256.hexdigest(values.join)
|
374
388
|
else
|
375
389
|
raise StandardError, "unexpected subject (#{subject_type})"
|
@@ -516,7 +530,12 @@ module Rodauth
|
|
516
530
|
end
|
517
531
|
end
|
518
532
|
|
519
|
-
#
|
533
|
+
# OpenID Connect Core 1.0's 5.4 Requesting Claims using Scope Values:
|
534
|
+
# If standard claims (profile, email, etc) are requested as scope values in the Authorization Request,
|
535
|
+
# include in the response.
|
536
|
+
include_claims ||= (OIDC_SCOPES_MAP.keys & oauth_scopes).any?
|
537
|
+
|
538
|
+
# However, when no Access Token is issued (which is the case for the response_type value id_token),
|
520
539
|
# the resulting Claims are returned in the ID Token.
|
521
540
|
fill_with_account_claims(id_claims, account, oauth_scopes, param_or_nil("claims_locales")) if include_claims
|
522
541
|
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oidc_backchannel_logout, :OidBackchannelLogout) do
|
7
|
+
depends :logout, :oidc_logout_base
|
8
|
+
|
9
|
+
auth_value_method :oauth_logout_token_expires_in, 60 # 1 minute
|
10
|
+
auth_value_method :backchannel_logout_session_supported, true
|
11
|
+
auth_value_method :oauth_applications_backchannel_logout_uri_column, :backchannel_logout_uri
|
12
|
+
auth_value_method :oauth_applications_backchannel_logout_session_required_column, :backchannel_logout_session_required
|
13
|
+
|
14
|
+
auth_methods(
|
15
|
+
:perform_logout_requests
|
16
|
+
)
|
17
|
+
|
18
|
+
def logout
|
19
|
+
visited_sites = session[visited_sites_key]
|
20
|
+
|
21
|
+
return super unless visited_sites
|
22
|
+
|
23
|
+
oauth_applications = db[oauth_applications_table].where(oauth_applications_client_id_column => visited_sites.map(&:first))
|
24
|
+
.as_hash(oauth_applications_id_column)
|
25
|
+
|
26
|
+
logout_params = oauth_applications.flat_map do |_id, oauth_application|
|
27
|
+
logout_url = oauth_application[oauth_applications_backchannel_logout_uri_column]
|
28
|
+
|
29
|
+
next unless logout_url
|
30
|
+
|
31
|
+
client_id = oauth_application[oauth_applications_client_id_column]
|
32
|
+
|
33
|
+
sids = visited_sites.select { |cid, _| cid == client_id }.map(&:last)
|
34
|
+
|
35
|
+
sids.map do |sid|
|
36
|
+
logout_token = generate_logout_token(oauth_application, sid)
|
37
|
+
|
38
|
+
[logout_url, logout_token]
|
39
|
+
end
|
40
|
+
end.compact
|
41
|
+
|
42
|
+
perform_logout_requests(logout_params) unless logout_params.empty?
|
43
|
+
|
44
|
+
# now we can clear the session
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def generate_logout_token(oauth_application, sid)
|
51
|
+
issued_at = Time.now.to_i
|
52
|
+
|
53
|
+
logout_claims = {
|
54
|
+
iss: oauth_jwt_issuer, # issuer
|
55
|
+
iat: issued_at, # issued at
|
56
|
+
exp: issued_at + oauth_logout_token_expires_in,
|
57
|
+
aud: oauth_application[oauth_applications_client_id_column],
|
58
|
+
events: {
|
59
|
+
"http://schemas.openid.net/event/backchannel-logout": {}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
logout_claims[:sid] = sid if sid
|
64
|
+
|
65
|
+
signing_algorithm = oauth_application[oauth_applications_id_token_signed_response_alg_column] ||
|
66
|
+
oauth_jwt_keys.keys.first
|
67
|
+
|
68
|
+
params = {
|
69
|
+
jwks: oauth_application_jwks(oauth_application),
|
70
|
+
headers: { typ: "logout+jwt" },
|
71
|
+
signing_algorithm: signing_algorithm,
|
72
|
+
encryption_algorithm: oauth_application[oauth_applications_id_token_encrypted_response_alg_column],
|
73
|
+
encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
|
74
|
+
}.compact
|
75
|
+
|
76
|
+
jwt_encode(logout_claims, **params)
|
77
|
+
end
|
78
|
+
|
79
|
+
def perform_logout_requests(logout_params)
|
80
|
+
# performs logout requests sequentially
|
81
|
+
logout_params.each do |logout_url, logout_token|
|
82
|
+
http_request(logout_url, { "logout_token" => logout_token })
|
83
|
+
rescue StandardError
|
84
|
+
warn "failed to perform backchannel logout on #{logout_url}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def id_token_claims(oauth_grant, signing_algorithm)
|
89
|
+
claims = super
|
90
|
+
|
91
|
+
return claims unless oauth_application[oauth_applications_backchannel_logout_uri_column]
|
92
|
+
|
93
|
+
session_id_in_claims(oauth_grant, claims)
|
94
|
+
|
95
|
+
claims
|
96
|
+
end
|
97
|
+
|
98
|
+
def should_set_oauth_application_in_visited_sites?
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def should_set_sid_in_visited_sites?(oauth_application)
|
103
|
+
super || requires_backchannel_logout_session?(oauth_application)
|
104
|
+
end
|
105
|
+
|
106
|
+
def requires_backchannel_logout_session?(oauth_application)
|
107
|
+
(
|
108
|
+
oauth_application &&
|
109
|
+
oauth_application[oauth_applications_backchannel_logout_session_required_column]
|
110
|
+
) || backchannel_logout_session_supported
|
111
|
+
end
|
112
|
+
|
113
|
+
def oauth_server_metadata_body(*)
|
114
|
+
super.tap do |data|
|
115
|
+
data[:backchannel_logout_supported] = true
|
116
|
+
data[:backchannel_logout_session_supported] = backchannel_logout_session_supported
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -145,6 +145,31 @@ module Rodauth
|
|
145
145
|
end
|
146
146
|
end
|
147
147
|
|
148
|
+
if features.include?(:oidc_frontchannel_logout)
|
149
|
+
if (value = @oauth_application_params[oauth_applications_frontchannel_logout_uri_column]) && !check_valid_no_fragment_uri?(value)
|
150
|
+
register_throw_json_response_error("invalid_client_metadata",
|
151
|
+
register_invalid_uri_message(value))
|
152
|
+
end
|
153
|
+
|
154
|
+
if (value = @oauth_application_params[oauth_applications_frontchannel_logout_session_required_column])
|
155
|
+
@oauth_application_params[oauth_applications_frontchannel_logout_session_required_column] =
|
156
|
+
convert_to_boolean("frontchannel_logout_session_required", value)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
if features.include?(:oidc_backchannel_logout)
|
161
|
+
if (value = @oauth_application_params[oauth_applications_backchannel_logout_uri_column]) && !check_valid_no_fragment_uri?(value)
|
162
|
+
register_throw_json_response_error("invalid_client_metadata",
|
163
|
+
register_invalid_uri_message(value))
|
164
|
+
end
|
165
|
+
|
166
|
+
if @oauth_application_params.key?(oauth_applications_backchannel_logout_session_required_column)
|
167
|
+
value = @oauth_application_params[oauth_applications_backchannel_logout_session_required_column]
|
168
|
+
@oauth_application_params[oauth_applications_backchannel_logout_session_required_column] =
|
169
|
+
convert_to_boolean("backchannel_logout_session_required", value)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
148
173
|
if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_alg_column]) &&
|
149
174
|
!oauth_jwt_jwe_algorithms_supported.include?(value)
|
150
175
|
register_throw_json_response_error("invalid_client_metadata",
|