diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index 265f696507a..c8110ba27b9 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -187,6 +187,7 @@ def current_issuer def request_url url = URI.parse request.original_url + url.path = remap_auth_post_path(url.path) query_params = Rack::Utils.parse_nested_query url.query unless query_params['SAMLRequest'] orig_saml_request = saml_request.options[:get_params][:SAMLRequest] @@ -200,4 +201,11 @@ def request_url url.query = Rack::Utils.build_query(query_params).presence url.to_s end + + def remap_auth_post_path(path) + path_match = path.match(%r{/api/saml/authpost(?\d{4})}) + return path unless path_match.present? + + "/api/saml/auth#{path_match[:year]}" + end end diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index 0574eca8815..967535affcf 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -76,7 +76,7 @@ def identity_needs_decryption? def capture_analytics analytics_payload = @result.to_h.merge( - endpoint: request.env['PATH_INFO'], + endpoint: remap_auth_post_path(request.env['PATH_INFO']), idv: identity_needs_verification?, finish_profile: profile_needs_verification?, ) diff --git a/app/controllers/saml_post_controller.rb b/app/controllers/saml_post_controller.rb new file mode 100644 index 00000000000..a9c2d389e9e --- /dev/null +++ b/app/controllers/saml_post_controller.rb @@ -0,0 +1,14 @@ +class SamlPostController < ApplicationController + after_action -> { request.session_options[:skip] = true }, only: :auth + skip_before_action :verify_authenticity_token + + def auth + path_year = request.path[-4..-1] + path_method = "api_saml_authpost#{path_year}_url" + action_url = Rails.application.routes.url_helpers.send(path_method) + + form_params = params.permit(:SAMLRequest, :RelayState, :SigAlg, :Signature) + + render locals: { action_url: action_url, form_params: form_params }, layout: false + end +end diff --git a/app/services/saml_endpoint.rb b/app/services/saml_endpoint.rb index a8af9f16be7..944e1462592 100644 --- a/app/services/saml_endpoint.rb +++ b/app/services/saml_endpoint.rb @@ -55,7 +55,7 @@ def suffix @suffix ||= begin suffixes = self.class.endpoint_configs.map { |config| config[:suffix] } suffixes.find do |suffix| - request.path.match(/(metadata|auth|logout)#{suffix}$/) + request.path.match(/(metadata|auth(post)?|logout)#{suffix}$/) end end end diff --git a/app/views/saml_post/auth.html.erb b/app/views/saml_post/auth.html.erb new file mode 100644 index 00000000000..f1725f24135 --- /dev/null +++ b/app/views/saml_post/auth.html.erb @@ -0,0 +1,30 @@ + + + + + + <%= csrf_meta_tags %> + <%= stylesheet_link_tag 'application', media: 'all' %> + <%= javascript_packs_with_chunks_tag 'saml-post' %> + + +
+
+

+ <%= t('saml_idp.shared.saml_post_binding.heading') %> +

+ +

+ <%= t('saml_idp.shared.saml_post_binding.no_js') %> +

+ + <%= form_tag action_url, id: 'saml-post-binding' do %> + <% form_params.each do |key, val| %> + <%= hidden_field_tag(key, val) %> + <% end %> + <%= submit_tag t('forms.buttons.submit.default'), class: 'usa-button usa-button--wide usa-button--big' %> + <% end %> +
+
+ + diff --git a/config/routes.rb b/config/routes.rb index ae416899b5f..f5c6695b9eb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,7 +15,11 @@ SamlEndpoint.suffixes.each do |suffix| get "/api/saml/metadata#{suffix}" => 'saml_idp#metadata', format: false match "/api/saml/logout#{suffix}" => 'saml_idp#logout', via: %i[get post delete] - match "/api/saml/auth#{suffix}" => 'saml_idp#auth', via: %i[get post] + # JS-driven POST redirect route to preserve existing session + post "/api/saml/auth#{suffix}" => 'saml_post#auth' + # actual SAML handling POST route + post "/api/saml/authpost#{suffix}" => 'saml_idp#auth' + get "/api/saml/auth#{suffix}" => 'saml_idp#auth' end post '/api/service_provider' => 'service_provider#update' diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 4e5c12d8ad6..5c9768fd331 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -565,11 +565,18 @@ def name_id_version(format_urn) ial2: false, ial2_strict: false, ialmax: false, - request_url: @stored_request_url, + request_url: @stored_request_url.gsub('authpost', 'auth'), request_id: sp_request_id, requested_attributes: ['email'], ) end + + it 'correctly sets the request URL' do + post :auth, params: { 'SAMLRequest' => @saml_request } + session_request_url = session[:sp][:request_url] + + expect(session_request_url).to match(%r{/api/saml/auth\d{4}}) + end end context 'service provider is valid' do @@ -589,7 +596,7 @@ def name_id_version(format_urn) ial2: false, ial2_strict: false, ialmax: false, - request_url: @saml_request.request.original_url, + request_url: @saml_request.request.original_url.gsub('authpost', 'auth'), request_id: sp_request_id, requested_attributes: ['email'], ) diff --git a/spec/controllers/saml_post_controller_spec.rb b/spec/controllers/saml_post_controller_spec.rb new file mode 100644 index 00000000000..8ae21b62562 --- /dev/null +++ b/spec/controllers/saml_post_controller_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +describe SamlPostController do + describe 'POST /api/saml/auth' do + render_views + include ActionView::Helpers::FormTagHelper + + let(:form_action_regex) { / saml_request, + 'RelayState' => relay_state, + 'SigAlg' => sig_alg, + 'Signature' => signature, + } + + expect(response.body).to match(form_action_regex) + expect(response.body).to match(hidden_field_tag('SAMLRequest', saml_request)) + expect(response.body).to match(hidden_field_tag('RelayState', relay_state)) + expect(response.body).to match(hidden_field_tag('SigAlg', sig_alg)) + expect(response.body).to match(hidden_field_tag('Signature', signature)) + end + + it 'does not render extra parameters' do + post :auth, params: { 'Foo' => 'bar' } + + expect(response.body).not_to match(hidden_field_tag('Foo', 'bar')) + end + end +end diff --git a/spec/requests/saml_post_spec.rb b/spec/requests/saml_post_spec.rb new file mode 100644 index 00000000000..b39aac365dd --- /dev/null +++ b/spec/requests/saml_post_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +RSpec.describe 'SAML POST handling', type: :request do + include SamlAuthHelper + + describe 'POST /api/saml/auth' do + let(:cookie_regex) { /\A(?\w+)=/ } + + it 'does not set a session cookie' do + post saml_settings.idp_sso_target_url + new_cookies = response.header['Set-Cookie'].split("\n").map do |c| + cookie_regex.match(c)[:cookie] + end + + expect(new_cookies).not_to include('_upaya_session') + end + end +end diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb index c7a04fb1e89..fed8ea1a6be 100644 --- a/spec/support/saml_auth_helper.rb +++ b/spec/support/saml_auth_helper.rb @@ -187,8 +187,8 @@ def authn_request_post_params(settings = saml_settings, params = {}) def post_saml_authn_request(settings = saml_settings, params = {}) saml_authn_params = authn_request_post_params(settings, params) - response = page.driver.post(saml_settings.idp_sso_target_url, saml_authn_params) - visit response.location + page.driver.post(saml_settings.idp_sso_target_url, saml_authn_params) + click_button(t('forms.buttons.submit.default')) end def login_and_confirm_sp(user)