diff --git a/app/controllers/concerns/openid_connect_redirect_concern.rb b/app/controllers/concerns/openid_connect_redirect_concern.rb new file mode 100644 index 00000000000..6867792360d --- /dev/null +++ b/app/controllers/concerns/openid_connect_redirect_concern.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module OpenidConnectRedirectConcern + def oidc_redirect_method(issuer:, user_uuid:) + user_redirect_method_override = + IdentityConfig.store.openid_connect_redirect_uuid_override_map[user_uuid] + + sp_redirect_method_override = + IdentityConfig.store.openid_connect_redirect_issuer_override_map[issuer] + + user_redirect_method_override || sp_redirect_method_override || + IdentityConfig.store.openid_connect_redirect + end + + # We do not include Content-Security-Policy form-action headers if they are + # configured to be disabled and we are not using a server-side redirect. + def form_action_csp_disabled_and_not_server_side_redirect?(issuer:, user_uuid:) + !IdentityConfig.store.openid_connect_content_security_form_action_enabled && + oidc_redirect_method( + issuer: issuer, + user_uuid: user_uuid, + ) != 'server_side' + end +end diff --git a/app/controllers/concerns/secure_headers_concern.rb b/app/controllers/concerns/secure_headers_concern.rb index 512027fe20d..4abbcc112c7 100644 --- a/app/controllers/concerns/secure_headers_concern.rb +++ b/app/controllers/concerns/secure_headers_concern.rb @@ -1,15 +1,20 @@ # frozen_string_literal: true module SecureHeadersConcern + include OpenidConnectRedirectConcern extend ActiveSupport::Concern def apply_secure_headers_override return if stored_url_for_user.blank? - return unless IdentityConfig.store.openid_connect_content_security_form_action_enabled authorize_form = OpenidConnectAuthorizeForm.new(authorize_params) return unless authorize_form.valid? + return if form_action_csp_disabled_and_not_server_side_redirect?( + issuer: authorize_form.service_provider.issuer, + user_uuid: current_user&.uuid, + ) + override_form_action_csp(csp_uris) end diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 2e46ad869e9..074d1dbfb82 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -9,6 +9,7 @@ class AuthorizationController < ApplicationController include AuthorizationCountConcern include BillableEventTrackable include ForcedReauthenticationConcern + include OpenidConnectRedirectConcern before_action :build_authorize_form_from_params, only: [:index] before_action :block_biometric_requests_in_production, only: [:index] @@ -137,7 +138,11 @@ def build_authorize_form_from_params end def secure_headers_override - return unless IdentityConfig.store.openid_connect_content_security_form_action_enabled + return if form_action_csp_disabled_and_not_server_side_redirect?( + issuer: @authorize_form.service_provider.issuer, + user_uuid: current_user&.uuid, + ) + csp_uris = SecureHeadersAllowList.csp_with_sp_redirect_uris( @authorize_form.redirect_uri, @authorize_form.service_provider.redirect_uris, @@ -222,17 +227,7 @@ def track_events end def redirect_user(redirect_uri, issuer, user_uuid) - user_redirect_method_override = - IdentityConfig.store.openid_connect_redirect_uuid_override_map[user_uuid] - - sp_redirect_method_override = - IdentityConfig.store.openid_connect_redirect_issuer_override_map[issuer] - - redirect_method = - user_redirect_method_override || sp_redirect_method_override || - IdentityConfig.store.openid_connect_redirect - - case redirect_method + case oidc_redirect_method(issuer: issuer, user_uuid: user_uuid) when 'client_side' @oidc_redirect_uri = redirect_uri render( diff --git a/app/controllers/openid_connect/logout_controller.rb b/app/controllers/openid_connect/logout_controller.rb index 660b04a9467..5e677e6dae8 100644 --- a/app/controllers/openid_connect/logout_controller.rb +++ b/app/controllers/openid_connect/logout_controller.rb @@ -4,6 +4,7 @@ module OpenidConnect class LogoutController < ApplicationController include SecureHeadersConcern include FullyAuthenticatable + include OpenidConnectRedirectConcern before_action :set_devise_failure_redirect_for_concurrent_session_logout, only: [:index] before_action :confirm_two_factor_authenticated, only: [:delete] @@ -45,17 +46,7 @@ def set_devise_failure_redirect_for_concurrent_session_logout end def redirect_user(redirect_uri, issuer, user_uuid) - user_redirect_method_override = - IdentityConfig.store.openid_connect_redirect_uuid_override_map[user_uuid] - - sp_redirect_method_override = - IdentityConfig.store.openid_connect_redirect_issuer_override_map[issuer] - - redirect_method = - user_redirect_method_override || sp_redirect_method_override || - IdentityConfig.store.openid_connect_redirect - - case redirect_method + case oidc_redirect_method(issuer: issuer, user_uuid: user_uuid) when 'client_side' @oidc_redirect_uri = redirect_uri render( @@ -78,7 +69,10 @@ def redirect_user(redirect_uri, issuer, user_uuid) def apply_logout_secure_headers_override(redirect_uri, service_provider) return if service_provider.nil? || redirect_uri.nil? - return unless IdentityConfig.store.openid_connect_content_security_form_action_enabled + return if form_action_csp_disabled_and_not_server_side_redirect?( + issuer: service_provider.issuer, + user_uuid: current_user&.id, + ) uris = SecureHeadersAllowList.csp_with_sp_redirect_uris( redirect_uri, diff --git a/spec/requests/csp_spec.rb b/spec/requests/csp_spec.rb index 1d0d5028e0b..04c91b91b79 100644 --- a/spec/requests/csp_spec.rb +++ b/spec/requests/csp_spec.rb @@ -1,92 +1,194 @@ require 'rails_helper' -RSpec.describe 'content security policy', allowed_extra_analytics: [:*] do +RSpec.describe 'content security policy' do context 'on endpoints that will redirect to an SP' do - context 'when openid_connect_content_security_form_action_enabled is enabled' do + context 'when using client side OIDC redirect' do before do - allow(IdentityConfig.store).to( - receive(:openid_connect_content_security_form_action_enabled), - ).and_return(true) + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('client_side') end - it 'includes a CSP with a form action that will allow a redirect to the CSP' do - visit_password_form_with_sp - follow_redirect! - - content_security_policy = parse_content_security_policy - - expect(content_security_policy['default-src']).to eq("'self'") - expect(content_security_policy['base-uri']).to eq("'self'") - expect(content_security_policy['child-src']).to eq("'self'") - expect(content_security_policy['connect-src']).to eq("'self'") - expect(content_security_policy['font-src']).to eq("'self' data:") - expect(content_security_policy['form-action']).to eq( - "'self' http://localhost:7654 https://example.com http://www.example.com", - ) - expect(content_security_policy['img-src']).to eq( - "'self' data: login.gov https://s3.us-west-2.amazonaws.com", - ) - expect(content_security_policy['media-src']).to eq("'self'") - expect(content_security_policy['object-src']).to eq("'none'") - expect(content_security_policy['script-src']).to match( - /'self' 'unsafe-eval' 'nonce-[\w\d=\/+]+'/, - ) - expect(content_security_policy['style-src']).to match(/'self' 'nonce-[\w\d=\/+]+'/) + context 'when openid_connect_content_security_form_action_enabled is enabled' do + before do + allow(IdentityConfig.store).to( + receive(:openid_connect_content_security_form_action_enabled), + ).and_return(true) + end + + it 'includes a CSP with a form action that will allow a redirect to the CSP' do + visit_password_form_with_sp + follow_redirect! + + content_security_policy = parse_content_security_policy + + expect(content_security_policy['default-src']).to eq("'self'") + expect(content_security_policy['base-uri']).to eq("'self'") + expect(content_security_policy['child-src']).to eq("'self'") + expect(content_security_policy['connect-src']).to eq("'self'") + expect(content_security_policy['font-src']).to eq("'self' data:") + expect(content_security_policy['form-action']).to eq( + "'self' http://localhost:7654 https://example.com http://www.example.com", + ) + expect(content_security_policy['img-src']).to eq( + "'self' data: login.gov https://s3.us-west-2.amazonaws.com", + ) + expect(content_security_policy['media-src']).to eq("'self'") + expect(content_security_policy['object-src']).to eq("'none'") + expect(content_security_policy['script-src']).to match( + /'self' 'unsafe-eval' 'nonce-[\w\d=\/+]+'/, + ) + expect(content_security_policy['style-src']).to match(/'self' 'nonce-[\w\d=\/+]+'/) + end + + it 'uses logout SP to override CSP form action that will allow a redirect to the CSP' do + visit_password_form_with_sp + visit_logout_form_with_sp + + content_security_policy = parse_content_security_policy + + expect(content_security_policy['form-action']).to eq( + "'self' gov.gsa.openidconnect.test:", + ) + end end - it 'uses logout SP to override CSP form action that will allow a redirect to the CSP' do - visit_password_form_with_sp - visit_logout_form_with_sp - - content_security_policy = parse_content_security_policy - - expect(content_security_policy['form-action']).to eq( - "'self' gov.gsa.openidconnect.test:", - ) + context 'when openid_connect_content_security_form_action_enabled is disabled' do + before do + allow(IdentityConfig.store).to( + receive(:openid_connect_content_security_form_action_enabled), + ).and_return(false) + end + + it 'includes a CSP without SP hosts in form-action' do + visit_password_form_with_sp + follow_redirect! + + content_security_policy = parse_content_security_policy + + expect(content_security_policy['default-src']).to eq("'self'") + expect(content_security_policy['base-uri']).to eq("'self'") + expect(content_security_policy['child-src']).to eq("'self'") + expect(content_security_policy['connect-src']).to eq("'self'") + expect(content_security_policy['font-src']).to eq("'self' data:") + expect(content_security_policy['form-action']).to eq( + "'self'", + ) + expect(content_security_policy['img-src']).to eq( + "'self' data: login.gov https://s3.us-west-2.amazonaws.com", + ) + expect(content_security_policy['media-src']).to eq("'self'") + expect(content_security_policy['object-src']).to eq("'none'") + expect(content_security_policy['script-src']).to match( + /'self' 'unsafe-eval' 'nonce-[\w\d=\/+]+'/, + ) + expect(content_security_policy['style-src']).to match(/'self' 'nonce-[\w\d=\/+]+'/) + end + + it 'uses logout SP to override CSP form action that will allow a redirect to the CSP' do + visit_password_form_with_sp + visit_logout_form_with_sp + + content_security_policy = parse_content_security_policy + + expect(content_security_policy['form-action']).to eq( + "'self'", + ) + end end end - context 'when openid_connect_content_security_form_action_enabled is disabled' do + context 'when using server side OIDC redirect' do before do - allow(IdentityConfig.store).to( - receive(:openid_connect_content_security_form_action_enabled), - ).and_return(false) + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') end - it 'includes a CSP without SP hosts in form-action' do - visit_password_form_with_sp - follow_redirect! - - content_security_policy = parse_content_security_policy - - expect(content_security_policy['default-src']).to eq("'self'") - expect(content_security_policy['base-uri']).to eq("'self'") - expect(content_security_policy['child-src']).to eq("'self'") - expect(content_security_policy['connect-src']).to eq("'self'") - expect(content_security_policy['font-src']).to eq("'self' data:") - expect(content_security_policy['form-action']).to eq( - "'self'", - ) - expect(content_security_policy['img-src']).to eq( - "'self' data: login.gov https://s3.us-west-2.amazonaws.com", - ) - expect(content_security_policy['media-src']).to eq("'self'") - expect(content_security_policy['object-src']).to eq("'none'") - expect(content_security_policy['script-src']).to match( - /'self' 'unsafe-eval' 'nonce-[\w\d=\/+]+'/, - ) - expect(content_security_policy['style-src']).to match(/'self' 'nonce-[\w\d=\/+]+'/) + context 'when openid_connect_content_security_form_action_enabled is enabled' do + before do + allow(IdentityConfig.store).to( + receive(:openid_connect_content_security_form_action_enabled), + ).and_return(true) + end + + it 'includes a CSP with a form action that will allow a redirect to the CSP' do + visit_password_form_with_sp + follow_redirect! + + content_security_policy = parse_content_security_policy + + expect(content_security_policy['default-src']).to eq("'self'") + expect(content_security_policy['base-uri']).to eq("'self'") + expect(content_security_policy['child-src']).to eq("'self'") + expect(content_security_policy['connect-src']).to eq("'self'") + expect(content_security_policy['font-src']).to eq("'self' data:") + expect(content_security_policy['form-action']).to eq( + "'self' http://localhost:7654 https://example.com http://www.example.com", + ) + expect(content_security_policy['img-src']).to eq( + "'self' data: login.gov https://s3.us-west-2.amazonaws.com", + ) + expect(content_security_policy['media-src']).to eq("'self'") + expect(content_security_policy['object-src']).to eq("'none'") + expect(content_security_policy['script-src']).to match( + /'self' 'unsafe-eval' 'nonce-[\w\d=\/+]+'/, + ) + expect(content_security_policy['style-src']).to match(/'self' 'nonce-[\w\d=\/+]+'/) + end + + it 'uses logout SP to override CSP form action that will allow a redirect to the CSP' do + visit_password_form_with_sp + visit_logout_form_with_sp + + content_security_policy = parse_content_security_policy + + expect(content_security_policy['form-action']).to eq( + "'self' gov.gsa.openidconnect.test:", + ) + end end - it 'uses logout SP to override CSP form action that will allow a redirect to the CSP' do - visit_password_form_with_sp - visit_logout_form_with_sp - - content_security_policy = parse_content_security_policy - - expect(content_security_policy['form-action']).to eq( - "'self'", - ) + context 'when openid_connect_content_security_form_action_enabled is disabled' do + before do + allow(IdentityConfig.store).to( + receive(:openid_connect_content_security_form_action_enabled), + ).and_return(false) + end + + it 'includes a CSP with SP hosts in form-action' do + visit_password_form_with_sp + follow_redirect! + + content_security_policy = parse_content_security_policy + + expect(content_security_policy['default-src']).to eq("'self'") + expect(content_security_policy['base-uri']).to eq("'self'") + expect(content_security_policy['child-src']).to eq("'self'") + expect(content_security_policy['connect-src']).to eq("'self'") + expect(content_security_policy['font-src']).to eq("'self' data:") + expect(content_security_policy['form-action']).to eq( + "'self' http://localhost:7654 https://example.com http://www.example.com", + ) + expect(content_security_policy['img-src']).to eq( + "'self' data: login.gov https://s3.us-west-2.amazonaws.com", + ) + expect(content_security_policy['media-src']).to eq("'self'") + expect(content_security_policy['object-src']).to eq("'none'") + expect(content_security_policy['script-src']).to match( + /'self' 'unsafe-eval' 'nonce-[\w\d=\/+]+'/, + ) + expect(content_security_policy['style-src']).to match(/'self' 'nonce-[\w\d=\/+]+'/) + end + + it 'uses logout SP to override CSP form action that will allow a redirect to the CSP' do + visit_password_form_with_sp + visit_logout_form_with_sp + + content_security_policy = parse_content_security_policy + + expect(content_security_policy['form-action']).to eq( + "'self' gov.gsa.openidconnect.test:", + ) + end end end end