rodauth-oauth 1.3.2 → 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 +17 -10
 - 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 +20 -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 +1 -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 +15 -10
 - 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_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 +11 -2
 
| 
         @@ -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",
         
     | 
| 
         @@ -0,0 +1,134 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "rodauth/oauth"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            # :nocov:
         
     | 
| 
      
 6 
     | 
    
         
            +
            raise LoadError, "the `:oidc_frontchannel_logout` requires rodauth 2.32.0 or higher" if Rodauth::VERSION < "2.32.0"
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            # :nocov:
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            module Rodauth
         
     | 
| 
      
 11 
     | 
    
         
            +
              Feature.define(:oidc_frontchannel_logout, :OidFrontchannelLogout) do
         
     | 
| 
      
 12 
     | 
    
         
            +
                depends :logout, :oidc_logout_base
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                view "frontchannel_logout", "Logout", "frontchannel_logout"
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                translatable_method :oauth_frontchannel_logout_redirecting_lead, "You are being redirected..."
         
     | 
| 
      
 17 
     | 
    
         
            +
                translatable_method :oauth_frontchannel_logout_redirecting_label, "please click %<link>s if your browser does not " \
         
     | 
| 
      
 18 
     | 
    
         
            +
                                                                                  "redirect you in a few seconds."
         
     | 
| 
      
 19 
     | 
    
         
            +
                translatable_method :oauth_frontchannel_logout_redirecting_link_label, "here"
         
     | 
| 
      
 20 
     | 
    
         
            +
                auth_value_method :frontchannel_logout_session_supported, true
         
     | 
| 
      
 21 
     | 
    
         
            +
                auth_value_method :frontchannel_logout_redirect_timeout, 5
         
     | 
| 
      
 22 
     | 
    
         
            +
                auth_value_method :oauth_applications_frontchannel_logout_uri_column, :frontchannel_logout_uri
         
     | 
| 
      
 23 
     | 
    
         
            +
                auth_value_method :oauth_applications_frontchannel_logout_session_required_column, :frontchannel_logout_session_required
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                attr_reader :frontchannel_logout_urls
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                attr_reader :frontchannel_logout_redirect
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                def logout
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @visited_sites = session[visited_sites_key]
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  super
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                def _logout_response
         
     | 
| 
      
 36 
     | 
    
         
            +
                  visited_sites = @visited_sites
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  return super unless visited_sites
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  logout_urls = db[oauth_applications_table]
         
     | 
| 
      
 41 
     | 
    
         
            +
                                .where(oauth_applications_client_id_column => visited_sites.map(&:first))
         
     | 
| 
      
 42 
     | 
    
         
            +
                                .as_hash(oauth_applications_client_id_column, oauth_applications_frontchannel_logout_uri_column)
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  return super if logout_urls.empty?
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  generate_frontchannel_logout_urls(visited_sites, logout_urls)
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  @frontchannel_logout_redirect = logout_redirect
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  set_notice_flash logout_notice_flash
         
     | 
| 
      
 51 
     | 
    
         
            +
                  return_response frontchannel_logout_view
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                # overrides rp-initiate logout response
         
     | 
| 
      
 55 
     | 
    
         
            +
                def _oidc_logout_response
         
     | 
| 
      
 56 
     | 
    
         
            +
                  visited_sites = @visited_sites
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                  return super unless visited_sites
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                  logout_urls = db[oauth_applications_table]
         
     | 
| 
      
 61 
     | 
    
         
            +
                                .where(oauth_applications_client_id_column => visited_sites.map(&:first))
         
     | 
| 
      
 62 
     | 
    
         
            +
                                .as_hash(oauth_applications_client_id_column, oauth_applications_frontchannel_logout_uri_column)
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                  return super if logout_urls.empty?
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                  generate_frontchannel_logout_urls(visited_sites, logout_urls)
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                  @frontchannel_logout_redirect = oidc_logout_redirect
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                  set_notice_flash logout_notice_flash
         
     | 
| 
      
 71 
     | 
    
         
            +
                  return_response frontchannel_logout_view
         
     | 
| 
      
 72 
     | 
    
         
            +
                end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                private
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                def generate_frontchannel_logout_urls(visited_sites, logout_urls)
         
     | 
| 
      
 77 
     | 
    
         
            +
                  @frontchannel_logout_urls = logout_urls.flat_map do |client_id, logout_url|
         
     | 
| 
      
 78 
     | 
    
         
            +
                    next unless logout_url
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                    sids = visited_sites.select { |cid, _| cid == client_id }.map(&:last)
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                    sids.map do |sid|
         
     | 
| 
      
 83 
     | 
    
         
            +
                      logout_url = URI(logout_url)
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                      if sid
         
     | 
| 
      
 86 
     | 
    
         
            +
                        query = logout_url.query
         
     | 
| 
      
 87 
     | 
    
         
            +
                        query = if query
         
     | 
| 
      
 88 
     | 
    
         
            +
                                  URI.decode_www_form(query)
         
     | 
| 
      
 89 
     | 
    
         
            +
                                else
         
     | 
| 
      
 90 
     | 
    
         
            +
                                  []
         
     | 
| 
      
 91 
     | 
    
         
            +
                                end
         
     | 
| 
      
 92 
     | 
    
         
            +
                        query << ["iss", oauth_jwt_issuer]
         
     | 
| 
      
 93 
     | 
    
         
            +
                        query << ["sid", sid]
         
     | 
| 
      
 94 
     | 
    
         
            +
                        logout_url.query = URI.encode_www_form(query)
         
     | 
| 
      
 95 
     | 
    
         
            +
                      end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                      logout_url
         
     | 
| 
      
 98 
     | 
    
         
            +
                    end
         
     | 
| 
      
 99 
     | 
    
         
            +
                  end.compact
         
     | 
| 
      
 100 
     | 
    
         
            +
                end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                def id_token_claims(oauth_grant, signing_algorithm)
         
     | 
| 
      
 103 
     | 
    
         
            +
                  claims = super
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                  return claims unless oauth_application[oauth_applications_frontchannel_logout_uri_column]
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                  session_id_in_claims(oauth_grant, claims)
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                  claims
         
     | 
| 
      
 110 
     | 
    
         
            +
                end
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                def should_set_oauth_application_in_visited_sites?
         
     | 
| 
      
 113 
     | 
    
         
            +
                  true
         
     | 
| 
      
 114 
     | 
    
         
            +
                end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                def should_set_sid_in_visited_sites?(oauth_application)
         
     | 
| 
      
 117 
     | 
    
         
            +
                  super || requires_frontchannel_logout_session?(oauth_application)
         
     | 
| 
      
 118 
     | 
    
         
            +
                end
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
                def requires_frontchannel_logout_session?(oauth_application)
         
     | 
| 
      
 121 
     | 
    
         
            +
                  (
         
     | 
| 
      
 122 
     | 
    
         
            +
                    oauth_application &&
         
     | 
| 
      
 123 
     | 
    
         
            +
                    oauth_application[oauth_applications_frontchannel_logout_session_required_column]
         
     | 
| 
      
 124 
     | 
    
         
            +
                  ) || frontchannel_logout_session_supported
         
     | 
| 
      
 125 
     | 
    
         
            +
                end
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                def oauth_server_metadata_body(*)
         
     | 
| 
      
 128 
     | 
    
         
            +
                  super.tap do |data|
         
     | 
| 
      
 129 
     | 
    
         
            +
                    data[:frontchannel_logout_supported] = true
         
     | 
| 
      
 130 
     | 
    
         
            +
                    data[:frontchannel_logout_session_supported] = frontchannel_logout_session_supported
         
     | 
| 
      
 131 
     | 
    
         
            +
                  end
         
     | 
| 
      
 132 
     | 
    
         
            +
                end
         
     | 
| 
      
 133 
     | 
    
         
            +
              end
         
     | 
| 
      
 134 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,76 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "rodauth/oauth"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Rodauth
         
     | 
| 
      
 6 
     | 
    
         
            +
              Feature.define(:oidc_logout_base, :OidcLogoutBase) do
         
     | 
| 
      
 7 
     | 
    
         
            +
                depends :oidc
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                session_key :visited_sites_key, :visited_sites
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                private
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                # set application/sid in visited sites when required
         
     | 
| 
      
 14 
     | 
    
         
            +
                def create_oauth_grant(create_params = {})
         
     | 
| 
      
 15 
     | 
    
         
            +
                  sid_in_visited_sites
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  super
         
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                def active_sessions?(session_id)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  !active_sessions_ds.where(active_sessions_session_id_column => session_id).empty?
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                def session_id_in_claims(oauth_grant, claims)
         
     | 
| 
      
 25 
     | 
    
         
            +
                  oauth_application_in_visited_sites do
         
     | 
| 
      
 26 
     | 
    
         
            +
                    if should_set_sid_in_visited_sites?(oauth_application)
         
     | 
| 
      
 27 
     | 
    
         
            +
                      # id_token or token response types
         
     | 
| 
      
 28 
     | 
    
         
            +
                      session_id = if (sess = session[session_id_session_key])
         
     | 
| 
      
 29 
     | 
    
         
            +
                                     compute_hmac(sess)
         
     | 
| 
      
 30 
     | 
    
         
            +
                                   else
         
     | 
| 
      
 31 
     | 
    
         
            +
                                     # code response type
         
     | 
| 
      
 32 
     | 
    
         
            +
                                     ds = db[active_sessions_table]
         
     | 
| 
      
 33 
     | 
    
         
            +
                                     ds = ds.where(active_sessions_account_id_column => oauth_grant[oauth_grants_account_id_column])
         
     | 
| 
      
 34 
     | 
    
         
            +
                                     ds = ds.order(Sequel.desc(active_sessions_last_use_column))
         
     | 
| 
      
 35 
     | 
    
         
            +
                                     ds.get(active_sessions_session_id_column)
         
     | 
| 
      
 36 
     | 
    
         
            +
                                   end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                      claims[:sid] = session_id
         
     | 
| 
      
 39 
     | 
    
         
            +
                    end
         
     | 
| 
      
 40 
     | 
    
         
            +
                  end
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                def oauth_application_in_visited_sites
         
     | 
| 
      
 44 
     | 
    
         
            +
                  visited_sites = session[visited_sites_key] || []
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  session_id = yield
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  visited_site = [oauth_application[oauth_applications_client_id_column], session_id]
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  return if visited_sites.include?(visited_site)
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  visited_sites << visited_site
         
     | 
| 
      
 53 
     | 
    
         
            +
                  set_session_value(visited_sites_key, visited_sites)
         
     | 
| 
      
 54 
     | 
    
         
            +
                end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                def sid_in_visited_sites
         
     | 
| 
      
 57 
     | 
    
         
            +
                  return unless should_set_oauth_application_in_visited_sites?
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                  oauth_application_in_visited_sites do
         
     | 
| 
      
 60 
     | 
    
         
            +
                    if should_set_sid_in_visited_sites?(oauth_application)
         
     | 
| 
      
 61 
     | 
    
         
            +
                      ds = active_sessions_ds.order(Sequel.desc(active_sessions_last_use_column))
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                      ds.get(active_sessions_session_id_column)
         
     | 
| 
      
 64 
     | 
    
         
            +
                    end
         
     | 
| 
      
 65 
     | 
    
         
            +
                  end
         
     | 
| 
      
 66 
     | 
    
         
            +
                end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                def should_set_oauth_application_in_visited_sites?
         
     | 
| 
      
 69 
     | 
    
         
            +
                  false
         
     | 
| 
      
 70 
     | 
    
         
            +
                end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                def should_set_sid_in_visited_sites?(*)
         
     | 
| 
      
 73 
     | 
    
         
            +
                  false
         
     | 
| 
      
 74 
     | 
    
         
            +
                end
         
     | 
| 
      
 75 
     | 
    
         
            +
              end
         
     | 
| 
      
 76 
     | 
    
         
            +
            end
         
     |