Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/controllers/concerns/openid_connect_redirect_concern.rb
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion app/controllers/concerns/secure_headers_concern.rb
Original file line number Diff line number Diff line change
@@ -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

Expand Down
19 changes: 7 additions & 12 deletions app/controllers/openid_connect/authorization_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
18 changes: 6 additions & 12 deletions app/controllers/openid_connect/logout_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand Down
248 changes: 175 additions & 73 deletions spec/requests/csp_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down