diff --git a/app/controllers/manual_verifications_controller.rb b/app/controllers/manual_verifications_controller.rb index 6645abc4..c9ca4201 100644 --- a/app/controllers/manual_verifications_controller.rb +++ b/app/controllers/manual_verifications_controller.rb @@ -98,6 +98,10 @@ def start_capture # direct upload fallback for either document class def submit_documents + # the scan button shares this form (per-form csrf tokens rule out a + # formaction to another url) and marks itself with intent=scan + return prepare_capture if params[:intent] == "scan" + unless @case.link_sent? && @case.document_class.present? redirect_to manual_verification_path and return end diff --git a/app/views/manual_verifications/show.html.erb b/app/views/manual_verifications/show.html.erb index 910986a8..023057d1 100644 --- a/app/views/manual_verifications/show.html.erb +++ b/app/views/manual_verifications/show.html.erb @@ -181,7 +181,10 @@ <% if @case.persona_capture_available? %> - + <%# same action as the form on purpose: rails per-form csrf tokens are + bound to the form url, so a formaction elsewhere fails forgery + protection. submit_documents dispatches on intent instead. %> +
Camera not working? Upload directly below.
<% end %> diff --git a/spec/requests/manual_verifications_spec.rb b/spec/requests/manual_verifications_spec.rb index 8e412e58..0530d96e 100644 --- a/spec/requests/manual_verifications_spec.rb +++ b/spec/requests/manual_verifications_spec.rb @@ -274,7 +274,9 @@ it "offers the scan button inside the same form as the details and consent" do get manual_verification_path expect(response.body).to include("Scan with your camera") - expect(response.body).to include(manual_verification_prepare_capture_path) + # must post to the shared form's own url: per-form csrf tokens reject a formaction elsewhere + expect(response.body).to include('name="intent" value="scan"') + expect(response.body).not_to include("formaction") expect(response.body).to include('name="biometric_consent"') end @@ -305,8 +307,26 @@ expect(kase.reload.submitted_fields).to be_empty end + it "passes real forgery protection when the scan button submits the shared form" do + # the test env disables csrf checks, which is how the formaction bug + # shipped: per-form tokens are bound to the form url, so the scan + # button must post to the same url as the upload form. + ActionController::Base.allow_forgery_protection = true + + get manual_verification_path + form = Nokogiri::HTML(response.body).at_css("form[action='#{manual_verification_documents_path}']") + token = form.at_css("input[name='authenticity_token']")["value"] + + post manual_verification_documents_path, + params: details.merge(attested: "1", biometric_consent: "1", intent: "scan", authenticity_token: token) + expect(response).to redirect_to(manual_verification_capture_path) + expect(kase.reload.attested).to be(true) + ensure + ActionController::Base.allow_forgery_protection = false + end + it "records the prerequisites on the case, then lets the capture start" do - post manual_verification_prepare_capture_path, params: details.merge(attested: "1", biometric_consent: "1") + post manual_verification_documents_path, params: details.merge(attested: "1", biometric_consent: "1", intent: "scan") expect(response).to redirect_to(manual_verification_capture_path) kase.reload