Skip to content

Commit

Permalink
Add the vtr to the SAML response when it is used as an AuthnContext…
Browse files Browse the repository at this point in the history
… for SAML

In #10178 we added the ability for SAML service providers to make a request with a vector of trust in the AuthnContext. When a SAML SP does this the vector of trust that is used for the authentication transaction should be reflected to the SP in the SAML response.

The authentication context appears in 2 places in the SAML request:

1. In the authn context for the entire transaction:

    ```xml
    <AuthnStatement AuthnInstant="2024-01-01T00:00:00" SessionIndex="_abc-123-def-456">
      <AuthnContext>
        <AuthnContextClassRef>A1.B2.C3</AuthnContextClassRef>
      </AuthnContext>
    </AuthnStatement>
    ```

2. In the attribute statement. With ACR values these appeared as seperated AAL and IAL nodes. For VTRs they appear in a single VTR node:

    ```xml
    <AttributeStatement>
      <!-- ... -->
      <Attribute Name="vtr" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="vtr">
        <AttributeValue>A1.B2.C3</AttributeValue>
      </Attribute>
    </AttributeStatement>
    ```

Making the VTR appear like this required changes in 2 places:

1. In the SAML controller the correct `authn_context` value was passed to the `#encode_response` method. This is a method from the `18f/saml_idp` gem which is overriden in `SamlIdpController`.
2. The `AttributeAsserter` was modified to recognized a VTR request and add the correct values to the `AttributeStatement` node in the SAML response.

changelog: Internal, SAML, VTR support was added to SAML Response
  • Loading branch information
jmhooper committed Mar 5, 2024
1 parent a34ce6e commit 14393a3
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 18 deletions.
8 changes: 5 additions & 3 deletions app/controllers/concerns/saml_idp_auth_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions app/controllers/saml_idp_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
44 changes: 30 additions & 14 deletions app/services/attribute_asserter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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_vtr(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
Expand All @@ -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
Expand Down Expand Up @@ -125,6 +132,11 @@ def add_verified_at(attrs)
attrs[:verified_at] = { getter: verified_at_getter_function }
end

def add_vtr(attrs)
context = resolved_authn_context_result.component_values.map(&:name).join('.')
attrs[:vtr] = { getter: vtr_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]
Expand All @@ -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
Expand All @@ -169,6 +181,10 @@ def verified_at_getter_function
->(principal) { principal.active_profile&.verified_at&.iso8601 }
end

def vtr_getter_function(vtr_authn_context)
->(_principal) { vtr_authn_context }
end

def aal_getter_function(aal_authn_context)
->(_principal) { aal_authn_context }
end
Expand Down
8 changes: 7 additions & 1 deletion spec/features/saml/vtr_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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('vtr').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

Expand Down Expand Up @@ -149,6 +153,8 @@
expect_successful_saml_redirect
end

scenario 'sign in with VTR request for idv includes proofed attributes'

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)

Expand Down
81 changes: 81 additions & 0 deletions spec/services/attribute_asserter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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: {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -295,6 +319,35 @@
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, :vtr],
)
expect(user.asserted_attributes[:first_name][:getter].call(user)).to eq 'Jåné'
expect(user.asserted_attributes[:vtr][:getter].call(user)).to eq 'C1.C2.P1'
end
end

context 'verified user and IAL1 request' do
let(:subject) do
described_class.new(
Expand Down Expand Up @@ -454,6 +507,34 @@
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, :vtr],
)
expect(user.asserted_attributes[:vtr][:getter].call(user)).to eq 'C1.C2'
end
end
end

context 'verified user and IAL1 AAL3 request' do
Expand Down

0 comments on commit 14393a3

Please sign in to comment.