diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index 4ac01f23005..3e4a139b7a1 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -129,8 +129,10 @@ def default_ial_context end end - def requested_aal_authn_context - saml_request.requested_aal_authn_context || default_aal_context + def response_authn_context + saml_request.requested_vtr_authn_context || + saml_request.requested_aal_authn_context || + default_aal_context end def requested_ial_authn_context @@ -186,7 +188,7 @@ def saml_response encode_response( current_user, name_id_format: name_id_format, - authn_context_classref: requested_aal_authn_context, + authn_context_classref: response_authn_context, reference_id: active_identity.session_uuid, encryption: encryption_opts, signature: saml_response_signature_options, diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index 98c879a6de1..e2d1ac49c3d 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -137,6 +137,7 @@ def log_external_saml_auth_request analytics.saml_auth_request( requested_ial: requested_ial, requested_aal_authn_context: saml_request&.requested_aal_authn_context, + requested_vtr_authn_context: saml_request&.requested_vtr_authn_context, force_authn: saml_request&.force_authn?, final_auth_request: sp_session[:final_auth_request], service_provider: saml_request&.issuer, diff --git a/app/services/attribute_asserter.rb b/app/services/attribute_asserter.rb index b1238b4d091..f700f9142af 100644 --- a/app/services/attribute_asserter.rb +++ b/app/services/attribute_asserter.rb @@ -35,9 +35,14 @@ def build add_email(attrs) if bundle.include? :email add_all_emails(attrs) if bundle.include? :all_emails add_bundle(attrs) if should_add_proofed_attributes? - add_verified_at(attrs) if bundle.include?(:verified_at) && ial_context.ial2_service_provider? - add_aal(attrs) - add_ial(attrs) if authn_request.requested_ial_authn_context || !service_provider.ial.nil? + add_verified_at(attrs) if bundle.include?(:verified_at) && ial2_service_provider? + if authn_request.requested_vtr_authn_context.present? + add_vot(attrs) + else + add_aal(attrs) + add_ial(attrs) if authn_request.requested_ial_authn_context || !service_provider.ial.nil? + end + add_x509(attrs) if bundle.include?(:x509_presented) && x509_data user.asserted_attributes = attrs end @@ -53,20 +58,22 @@ def build def should_add_proofed_attributes? return false if !user.active_profile.present? - ial_context.ial2_or_greater? || ial_max_requested? + resolved_authn_context_result.identity_proofing_or_ialmax? end - def ial_max_requested? - ial_acr_value = FederatedProtocols::Saml.new(authn_request).ial - Vot::LegacyComponentValues.by_name[ial_acr_value]&.requirements&.include?(:ialmax) + def ial2_service_provider? + service_provider.ial.to_i >= ::Idp::Constants::IAL2 end - def ial_context - @ial_context ||= IalContext.new( - ial: authn_context, - service_provider: service_provider, - user: user, - ) + def resolved_authn_context_result + @resolved_authn_context_result ||= begin + saml = FederatedProtocols::Saml.new(authn_request) + AuthnContextResolver.new( + service_provider: service_provider, + vtr: saml.vtr, + acr_values: saml.acr_values, + ).resolve + end end def default_attrs @@ -125,6 +132,11 @@ def add_verified_at(attrs) attrs[:verified_at] = { getter: verified_at_getter_function } end + def add_vot(attrs) + context = resolved_authn_context_result.component_values.map(&:name).join('.') + attrs[:vot] = { getter: vot_getter_function(context) } + end + def add_aal(attrs) requested_context = authn_request.requested_aal_authn_context requested_aal_level = Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_AAL[requested_context] @@ -145,7 +157,7 @@ def add_ial(attrs) end def ialmax_requested_and_fullfilable? - ial_max_requested? && user.active_profile.present? + resolved_authn_context_result.ialmax? && user.active_profile.present? end def sp_ial @@ -169,6 +181,10 @@ def verified_at_getter_function ->(principal) { principal.active_profile&.verified_at&.iso8601 } end + def vot_getter_function(vot_authn_context) + ->(_principal) { vot_authn_context } + end + def aal_getter_function(aal_authn_context) ->(_principal) { aal_authn_context } end diff --git a/spec/features/saml/vtr_spec.rb b/spec/features/saml/vtr_spec.rb index 3d95f889bfc..48a75bf953d 100644 --- a/spec/features/saml/vtr_spec.rb +++ b/spec/features/saml/vtr_spec.rb @@ -32,8 +32,12 @@ expect_successful_saml_redirect xmldoc = SamlResponseDoc.new('feature', 'response_assertion') - email = xmldoc.attribute_node_for('email').children.map(&:text).join + expect(xmldoc.assertion_statement_node.content).to eq('C1') + expect(xmldoc.attribute_node_for('vot').content).to eq('C1') + expect(xmldoc.attribute_node_for('ial')).to be_nil + expect(xmldoc.attribute_node_for('aal')).to be_nil + email = xmldoc.attribute_node_for('email').content expect(user.email_addresses.first.email).to eq(email) end @@ -149,6 +153,44 @@ expect_successful_saml_redirect end + scenario 'sign in with VTR request for idv includes proofed attributes' do + pii = { + first_name: 'Jonathan', + ssn: '900-66-6666', + } + user = create(:user, :fully_registered) + create(:profile, :active, user: user, pii: pii) + + visit_saml_authn_request_url( + overrides: { + issuer: sp1_issuer, + authn_context: [ + 'C1.C2.P1', + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name", + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}ssn", + ], + }, + ) + sign_in_live_with_2fa(user) + click_submit_default + click_agree_and_continue + click_submit_default + + expect_successful_saml_redirect + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + expect(xmldoc.assertion_statement_node.content).to eq('C1.C2.P1') + expect(xmldoc.attribute_node_for('vot').content).to eq('C1.C2.P1') + expect(xmldoc.attribute_node_for('ial')).to be_nil + expect(xmldoc.attribute_node_for('aal')).to be_nil + + first_name = xmldoc.attribute_node_for('first_name').content + ssn = xmldoc.attribute_node_for('ssn').content + + expect(first_name).to eq(pii[:first_name]) + expect(ssn).to eq(pii[:ssn]) + end + scenario 'sign in with VTR request for idv with biometric requires idv with biometric', :js do allow(IdentityConfig.store).to receive(:doc_auth_selfie_capture_enabled).and_return(true) diff --git a/spec/services/attribute_asserter_spec.rb b/spec/services/attribute_asserter_spec.rb index f991fe6d162..abe490c707c 100644 --- a/spec/services/attribute_asserter_spec.rb +++ b/spec/services/attribute_asserter_spec.rb @@ -50,6 +50,15 @@ ) CGI.unescape ial1_authn_request_url.split('SAMLRequest').last end + let(:raw_vtr_no_proofing_authn_request) do + vtr_proofing_authn_request = saml_authn_request_url( + overrides: { + issuer: sp1_issuer, + authn_context: 'C1.C2', + }, + ) + CGI.unescape vtr_proofing_authn_request.split('SAMLRequest').last + end let(:raw_ial2_authn_request) do ial2_authnrequest = saml_authn_request_url( overrides: { @@ -59,6 +68,15 @@ ) CGI.unescape ial2_authnrequest.split('SAMLRequest').last end + let(:raw_vtr_proofing_authn_request) do + vtr_proofing_authn_request = saml_authn_request_url( + overrides: { + issuer: sp1_issuer, + authn_context: 'C1.C2.P1', + }, + ) + CGI.unescape vtr_proofing_authn_request.split('SAMLRequest').last + end let(:raw_ial1_aal3_authn_request) do ial1_aal3_authnrequest = saml_authn_request_url( overrides: { @@ -95,6 +113,12 @@ let(:ial2_authn_request) do SamlIdp::Request.from_deflated_request(raw_ial2_authn_request) end + let(:vtr_proofing_authn_request) do + SamlIdp::Request.from_deflated_request(raw_vtr_proofing_authn_request) + end + let(:vtr_no_proofing_authn_request) do + SamlIdp::Request.from_deflated_request(raw_vtr_no_proofing_authn_request) + end let(:ial1_aal3_authn_request) do SamlIdp::Request.from_deflated_request(raw_ial1_aal3_authn_request) end @@ -295,6 +319,34 @@ end end + context 'verified user and proofing VTR request' do + let(:subject) do + described_class.new( + user: user, + name_id_format: name_id_format, + service_provider: service_provider, + authn_request: vtr_proofing_authn_request, + decrypted_pii: decrypted_pii, + user_session: user_session, + ) + end + + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email first_name last_name]) + subject.build + end + + it 'includes the correct bundle attributes' do + expect(user.asserted_attributes.keys).to eq( + [:uuid, :email, :first_name, :last_name, :verified_at, :vot], + ) + expect(user.asserted_attributes[:first_name][:getter].call(user)).to eq 'Jåné' + expect(user.asserted_attributes[:vot][:getter].call(user)).to eq 'C1.C2.P1' + end + end + context 'verified user and IAL1 request' do let(:subject) do described_class.new( @@ -454,6 +506,33 @@ end end end + + context 'request made with a VTR param' do + let(:subject) do + described_class.new( + user: user, + name_id_format: name_id_format, + service_provider: service_provider, + authn_request: vtr_no_proofing_authn_request, + decrypted_pii: decrypted_pii, + user_session: user_session, + ) + end + + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email]) + subject.build + end + + it 'includes the correct bundle attributes' do + expect(user.asserted_attributes.keys).to eq( + [:uuid, :email, :vot], + ) + expect(user.asserted_attributes[:vot][:getter].call(user)).to eq 'C1.C2' + end + end end context 'verified user and IAL1 AAL3 request' do