app/services/attribute_asserter.rb
# frozen_string_literal: true
require 'stringex/unidecoder'
require 'stringex/core_ext'
class AttributeAsserter
VALID_ATTRIBUTES = %i[
first_name
middle_name
last_name
address1
address2
city
state
zipcode
dob
ssn
phone
].freeze
def initialize(user:,
service_provider:,
name_id_format:,
authn_request:,
decrypted_pii:,
user_session:)
self.user = user
self.service_provider = service_provider
self.name_id_format = name_id_format
self.authn_request = authn_request
self.decrypted_pii = decrypted_pii
self.user_session = user_session
end
def build
attrs = default_attrs
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) && ial2_service_provider?
if authn_request.requested_vtr_authn_contexts.present?
add_vot(attrs)
else
add_aal(attrs)
add_ial(attrs)
end
add_x509(attrs) if bundle.include?(:x509_presented) && x509_data
user.asserted_attributes = attrs
end
private
attr_accessor :user,
:service_provider,
:name_id_format,
:authn_request,
:decrypted_pii,
:user_session
def should_add_proofed_attributes?
return false if !user.active_profile.present?
resolved_authn_context_result.identity_proofing_or_ialmax?
end
def ial2_service_provider?
service_provider.ial.to_i >= ::Idp::Constants::IAL2
end
def resolved_authn_context_result
authn_context_resolver.result
end
def authn_context_resolver
@authn_context_resolver ||= begin
saml = FederatedProtocols::Saml.new(authn_request)
AuthnContextResolver.new(
user: user,
service_provider: service_provider,
vtr: saml.vtr,
acr_values: saml.acr_values,
)
end
end
def default_attrs
{
uuid: {
getter: uuid_getter_function,
name_format: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
name_id_format: Saml::XML::Namespaces::Formats::NameId::PERSISTENT,
},
}
end
def add_bundle(attrs)
bundle.each do |attr|
next unless VALID_ATTRIBUTES.include? attr
getter = ascii? ? attribute_getter_function_ascii(attr) : attribute_getter_function(attr)
if attr == :phone
getter = wrap_with_phone_formatter(getter)
elsif attr == :zipcode
getter = wrap_with_zipcode_formatter(getter)
elsif attr == :dob
getter = wrap_with_dob_formatter(getter)
end
attrs[attr] = { getter: getter }
end
add_verified_at(attrs)
end
def wrap_with_phone_formatter(getter)
proc do |principal|
result = getter.call(principal)
if result.present?
Phonelib.parse(result).e164
else
result
end
end
end
def wrap_with_zipcode_formatter(getter)
proc do |principal|
getter.call(principal)&.strip&.slice(0, 5)
end
end
def wrap_with_dob_formatter(getter)
proc do |principal|
if (date_str = getter.call(principal))
DateParser.parse_legacy(date_str).to_s
end
end
end
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 = requested_aal_authn_context
requested_aal_level = Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_AAL[requested_context]
aal_level = requested_aal_level || service_provider.default_aal || ::Idp::Constants::DEFAULT_AAL
context = Saml::Idp::Constants::AUTHN_CONTEXT_AAL_TO_CLASSREF[aal_level]
attrs[:aal] = { getter: aal_getter_function(context) } if context
end
def add_ial(attrs)
asserted_ial = authn_context_resolver.asserted_ial_acr
attrs[:ial] = { getter: ial_getter_function(asserted_ial) } if asserted_ial
end
def sp_ial
Saml::Idp::Constants::AUTHN_CONTEXT_IAL_TO_CLASSREF[service_provider.ial]
end
def add_x509(attrs)
attrs[:x509_subject] = { getter: ->(_principal) { x509_data.subject } }
attrs[:x509_issuer] = { getter: ->(_principal) { x509_data.issuer } }
attrs[:x509_presented] = { getter: ->(_principal) { x509_data.presented } }
end
def uuid_getter_function
lambda do |principal|
identity = principal.active_identity_for(service_provider)
AgencyIdentityLinker.new(identity).link_identity.uuid
end
end
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
def ial_getter_function(ial_authn_context)
->(_principal) { ial_authn_context }
end
def attribute_getter_function(attr)
->(_principal) { decrypted_pii[attr] }
end
def attribute_getter_function_ascii(attr)
->(_principal) { decrypted_pii[attr].to_ascii }
end
def add_email(attrs)
attrs[:email] = {
getter: ->(principal) {
last_email_from_sp(principal) ||
EmailContext.new(principal).last_sign_in_email_address.email
},
name_format: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
name_id_format: Saml::XML::Namespaces::Formats::NameId::EMAIL_ADDRESS,
}
end
def add_all_emails(attrs)
attrs[:all_emails] = {
getter: ->(principal) { principal.confirmed_email_addresses.map(&:email) },
name_format: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
name_id_format: Saml::XML::Namespaces::Formats::NameId::EMAIL_ADDRESS,
}
end
def last_email_from_sp(principal)
return nil unless IdentityConfig.store.feature_select_email_to_share_enabled
identity = principal.active_identity_for(service_provider)
email_id = identity&.email_address_id
principal.confirmed_email_addresses.find_by(id: email_id)&.email if email_id
end
def bundle
@bundle ||= (
authn_request_bundle || service_provider.metadata[:attribute_bundle] || []
).map(&:to_sym)
end
def requested_ial_authn_context
FederatedProtocols::Saml.new(authn_request).requested_ial_authn_context
end
def requested_aal_authn_context
FederatedProtocols::Saml.new(authn_request).aal
end
def authn_request_bundle
SamlRequestParser.new(authn_request).requested_attributes
end
def x509_data
return @x509_data if defined?(@x509_data)
@x509_data ||= begin
x509_hash = user_session[:decrypted_x509]
X509::Attributes.new_from_json(x509_hash) if x509_hash
end
end
def ascii?
bundle.include?(:ascii)
end
end