From 24d95a389526103d5d7292360346c865704186ab Mon Sep 17 00:00:00 2001 From: Leo Wilkin Date: Tue, 6 Oct 2026 22:59:42 +0100 Subject: [PATCH] manual verification: scan button must post to the shared form's own url rails per-form csrf tokens (on under load_defaults 8.0) are bound to the form's action url. the scan button submitted via formaction to the prepare_capture route with the upload form's token, so every click raised InvalidAuthenticityToken in production. that exception is on sentry's default ignore list, which is why nothing showed up there, and forgery protection is off in the test env, which is how it shipped. the button now submits to the same url with intent=scan and submit_documents dispatches to prepare_capture. added a request spec that turns forgery protection on and submits the real form token. --- .../manual_verifications_controller.rb | 4 ++++ app/views/manual_verifications/show.html.erb | 5 +++- spec/requests/manual_verifications_spec.rb | 24 +++++++++++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) 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