diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 01944cf0858..930f7e25b42 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -174,7 +174,6 @@ def pre_validate_authorize_form user_fully_authenticated: user_fully_authenticated?, referer: request.referer, vtr_param: params[:vtr], - unknown_authn_contexts:, ), ) return if result.success? @@ -259,12 +258,5 @@ def redirect_user(redirect_uri, issuer, user_uuid) def sp_handoff_bouncer @sp_handoff_bouncer ||= SpHandoffBouncer.new(sp_session) end - - def unknown_authn_contexts - return nil if params[:vtr].present? || params[:acr_values].blank? - - (params[:acr_values].split - Saml::Idp::Constants::VALID_AUTHN_CONTEXTS). - join(' ').presence - end end end diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index a945f66259f..b4278026379 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -131,7 +131,6 @@ def capture_analytics request_signed: saml_request.signed?, matching_cert_serial:, requested_nameid_format: saml_request.name_id_format, - unknown_authn_contexts:, ) if result.success? && saml_request.signed? @@ -152,13 +151,12 @@ def log_external_saml_auth_request analytics.saml_auth_request( requested_ial: requested_ial, - authn_context: requested_authn_contexts, + authn_context: saml_request&.requested_authn_contexts, requested_aal_authn_context: FederatedProtocols::Saml.new(saml_request).aal, requested_vtr_authn_contexts: saml_request&.requested_vtr_authn_contexts.presence, force_authn: saml_request&.force_authn?, final_auth_request: sp_session[:final_auth_request], service_provider: saml_request&.issuer, - unknown_authn_contexts:, user_fully_authenticated: user_fully_authenticated?, ) end @@ -229,25 +227,4 @@ def resolved_authn_context_int_ial def require_path_year render_not_found if params[:path_year].blank? end - - def unknown_authn_contexts - return nil if saml_request.requested_vtr_authn_contexts.present? - return nil if requested_authn_contexts.blank? - - unmatched_authn_contexts.reject do |authn_context| - authn_context.match(req_attrs_regexp) - end.join(' ').presence - end - - def unmatched_authn_contexts - requested_authn_contexts - Saml::Idp::Constants::VALID_AUTHN_CONTEXTS - end - - def requested_authn_contexts - @request_authn_contexts || saml_request&.requested_authn_contexts - end - - def req_attrs_regexp - Regexp.escape(Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF) - end end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index c0f90702f37..d7175ac29ce 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -5498,7 +5498,6 @@ def openid_connect_bearer_token(success:, ial:, client_id:, errors:, error_detai # @param [String, nil] vtr_param # @param [Boolean] unauthorized_scope # @param [Boolean] user_fully_authenticated - # @param [String] unknown_authn_contexts space separated list of unknown contexts def openid_connect_request_authorization( success:, errors:, @@ -5515,7 +5514,6 @@ def openid_connect_request_authorization( unauthorized_scope:, user_fully_authenticated:, error_details: nil, - unknown_authn_contexts: nil, **extra ) track_event( @@ -5535,7 +5533,6 @@ def openid_connect_request_authorization( vtr_param:, unauthorized_scope:, user_fully_authenticated:, - unknown_authn_contexts:, **extra, ) end @@ -6337,7 +6334,6 @@ def rules_of_use_visit # matches the request certificate in a successful, signed request # @param [Hash] cert_error_details Details for errors that occurred because of an invalid # signature - # @param [String] unknown_authn_contexts space separated list of unknown contexts def saml_auth( success:, errors:, @@ -6354,7 +6350,6 @@ def saml_auth( matching_cert_serial:, error_details: nil, cert_error_details: nil, - unknown_authn_contexts: nil, **extra ) track_event( @@ -6374,7 +6369,6 @@ def saml_auth( request_signed:, matching_cert_serial:, cert_error_details:, - unknown_authn_contexts:, **extra, ) end @@ -6386,7 +6380,6 @@ def saml_auth( # @param [Boolean] force_authn # @param [Boolean] final_auth_request # @param [String] service_provider - # @param [String] unknown_authn_contexts space separated list of unknown contexts # @param [Boolean] user_fully_authenticated # An external request for SAML Authentication was received def saml_auth_request( @@ -6397,7 +6390,6 @@ def saml_auth_request( force_authn:, final_auth_request:, service_provider:, - unknown_authn_contexts:, user_fully_authenticated:, **extra ) @@ -6410,7 +6402,6 @@ def saml_auth_request( force_authn:, final_auth_request:, service_provider:, - unknown_authn_contexts:, user_fully_authenticated:, **extra, ) diff --git a/app/services/saml_request_validator.rb b/app/services/saml_request_validator.rb index 44826eb879c..740d67a5f60 100644 --- a/app/services/saml_request_validator.rb +++ b/app/services/saml_request_validator.rb @@ -88,10 +88,7 @@ def valid_authn_context? next true if classref.match?(SamlIdp::Request::VTR_REGEXP) && IdentityConfig.store.use_vot_in_sp_requests end - # SAML requests are allowed to "default" to the integration's IAL default. - return true if authn_contexts.empty? - - authn_contexts.any? do |classref| + authn_contexts.all? do |classref| valid_contexts.include?(classref) end end diff --git a/app/services/vot/parser.rb b/app/services/vot/parser.rb index 4b768f3ab7b..3f135fad867 100644 --- a/app/services/vot/parser.rb +++ b/app/services/vot/parser.rb @@ -4,6 +4,8 @@ module Vot class Parser class ParseException < StandardError; end + class UnsupportedComponentsException < ParseException; end + class DuplicateComponentsException < ParseException; end Result = Data.define( @@ -85,7 +87,8 @@ def initial_components @initial_components ||= component_string.split(component_separator).map do |component_name| component_map.fetch(component_name) rescue KeyError - end.compact + raise_unsupported_component_exception(component_name) + end end def component_separator @@ -110,6 +113,16 @@ def validate_component_uniqueness!(component_values) end end + def raise_unsupported_component_exception(component_value_name) + if vector_of_trust.present? + raise UnsupportedComponentsException, + "'#{vector_of_trust}' contains unknown component '#{component_value_name}'" + else + raise UnsupportedComponentsException, + "'#{acr_values}' contains unknown acr value '#{component_value_name}'" + end + end + def raise_duplicate_component_exception if vector_of_trust.present? raise DuplicateComponentsException, "'#{vector_of_trust}' contains duplicate components" diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index d3a0a0bff8f..be7775ef85d 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -477,7 +477,7 @@ def index let(:vtr) { nil } let(:acr_values) do [ - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + 'http://idmanagement.gov/ns/assurance/aal/1', ].join(' ') end @@ -486,31 +486,6 @@ def index expect(result.identity_proofing?).to eq(true) end - context 'when an unknown acr value is passed in' do - let(:acr_values) { 'unknown-acr-value' } - - it 'raises an exception' do - expect { result }.to raise_exception( - Vot::Parser::ParseException, - 'VoT parser called without VoT or ACR values', - ) - end - - context 'with a known acr value' do - let(:acr_values) do - [ - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, - 'unknown-acr-value', - ].join(' ') - end - - it 'returns a resolved authn context result' do - expect(result.aal2?).to eq(true) - expect(result.identity_proofing?).to eq(true) - end - end - end - context 'without an SP' do let(:sp) { nil } let(:sp_session) { nil } diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index 0590bca9983..16686795bc6 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -2029,65 +2029,6 @@ end end - context 'when there are unknown acr_values params' do - let(:unknown_value) { 'unknown-acr-value' } - let(:acr_values) { unknown_value } - - context 'when there is only an unknown acr_value' do - it 'tracks the event with errors' do - stub_analytics - - action - - expect(@analytics).to have_logged_event( - 'OpenID Connect: authorization request', - success: false, - client_id:, - prompt:, - allow_prompt_login: true, - unauthorized_scope: false, - errors: hash_including(:acr_values), - error_details: hash_including(:acr_values), - user_fully_authenticated: true, - acr_values: '', - code_challenge_present: false, - scope: 'openid profile', - unknown_authn_contexts: unknown_value, - ) - end - - context 'when there is also a valid acr_value' do - let(:known_value) { Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF } - let(:acr_values) do - [ - unknown_value, - known_value, - ].join(' ') - end - - it 'tracks the event' do - stub_analytics - - action - expect(@analytics).to have_logged_event( - 'OpenID Connect: authorization request', - success: true, - client_id:, - prompt:, - allow_prompt_login: true, - unauthorized_scope: false, - user_fully_authenticated: true, - acr_values: known_value, - code_challenge_present: false, - scope: 'openid profile', - unknown_authn_contexts: unknown_value, - errors: {}, - ) - end - end - end - end - context 'vtr with invalid params that do not interfere with the redirect_uri' do let(:acr_values) { nil } let(:vtr) { ['C1'].to_json } diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 9777cfbc734..5a19104dccb 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -970,22 +970,15 @@ def name_id_version(format_urn) end context 'authn_context is invalid' do - let(:unknown_value) do - 'http://idmanagement.gov/ns/assurance/loa/5' - end - let(:authn_context) { unknown_value } - - before do + it 'renders an error page' do stub_analytics saml_get_auth( saml_settings( - overrides: { authn_context: }, + overrides: { authn_context: 'http://idmanagement.gov/ns/assurance/loa/5' }, ), ) - end - it 'renders an error page' do expect(controller).to render_template('saml_idp/auth/error') expect(response.status).to eq(400) expect(response.body).to include(t('errors.messages.unauthorized_authn_context')) @@ -996,7 +989,7 @@ def name_id_version(format_urn) errors: { authn_context: [t('errors.messages.unauthorized_authn_context')] }, error_details: { authn_context: { unauthorized_authn_context: true } }, nameid_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, - authn_context: [unknown_value], + authn_context: ['http://idmanagement.gov/ns/assurance/loa/5'], authn_context_comparison: 'exact', service_provider: 'http://localhost:3000', request_signed: true, @@ -1005,49 +998,9 @@ def name_id_version(format_urn) idv: false, finish_profile: false, matching_cert_serial: saml_test_sp_cert_serial, - unknown_authn_contexts: unknown_value, ), ) end - - context 'there is also a valid authn_context' do - let(:authn_context) do - [ - Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, - unknown_value, - ] - end - - it 'logs the unknown authn_context value' do - expect(response.status).to eq(302) - expect(@analytics).to have_logged_event( - 'SAML Auth Request', - hash_including( - unknown_authn_contexts: unknown_value, - ), - ) - end - - context 'when it includes the ReqAttributes AuthnContext' do - let(:authn_context) do - [ - Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF, - Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, - unknown_value, - ] - end - - it 'logs the unknown authn_context value' do - expect(response.status).to eq(302) - expect(@analytics).to have_logged_event( - 'SAML Auth Request', - hash_including( - unknown_authn_contexts: unknown_value, - ), - ) - end - end - end end context 'authn_context scenarios' do diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 380037b22f4..6fb26fda4d7 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -187,35 +187,6 @@ end end - context 'with unknown acr_values' do - let(:acr_values) { 'unknown-value' } - let(:vtr) { nil } - - it 'has errors' do - expect(valid?).to eq(false) - expect(form.errors[:acr_values]). - to include(t('openid_connect.authorization.errors.no_valid_acr_values')) - end - - context 'with a known IAL value' do - before do - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return(client_id) - end - let(:acr_values) do - [ - 'unknown-value', - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, - ].join(' ') - end - - it 'is valid' do - expect(valid?).to eq(true) - end - end - end - context 'with ialmax requested' do let(:acr_values) { Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF } let(:vtr) { nil } @@ -240,53 +211,42 @@ end end - context 'when facial match is requested' do - shared_examples 'allows facial match IAL only if sp is authorized' do |facial_match_ial| - let(:acr_values) { facial_match_ial } - - context "when the IAL requested is #{facial_match_ial}" do - context 'when the service provider is allowed to use facial match ials' do - before do - allow(IdentityConfig.store).to receive( - :allowed_biometric_ial_providers, - ).and_return([client_id]) - end + shared_examples 'allows facial match IAL only if sp is authorized' do |facial_match_ial| + let(:acr_values) { facial_match_ial } - it 'succeeds validation' do - expect(form).to be_valid - end + context "when the IAL requested is #{facial_match_ial}" do + context 'when the service provider is allowed to use facial match ials' do + before do + allow_any_instance_of(ServiceProvider).to receive(:facial_match_ial_allowed?). + and_return(true) end - context 'when the service provider is not allowed to use facial match ials' do - it 'fails with a not authorized error' do - expect(form).not_to be_valid - expect(form.errors[:acr_values]). - to include(t('openid_connect.authorization.errors.no_auth')) - end + it 'succeeds validation' do + expect(form).to be_valid end end - end - - it_behaves_like 'allows facial match IAL only if sp is authorized', - Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF - it_behaves_like 'allows facial match IAL only if sp is authorized', - Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF + context 'when the service provider is not allowed to use facial match ials' do + before do + allow_any_instance_of(ServiceProvider).to receive(:facial_match_ial_allowed?). + and_return(false) + end - context 'when using semantic acr_values' do - before do - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([client_id]) + it 'fails with a not authorized error' do + expect(form).not_to be_valid + expect(form.errors[:acr_values]). + to include(t('openid_connect.authorization.errors.no_auth')) + end end - it_behaves_like 'allows facial match IAL only if sp is authorized', - Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_PREFERRED_ACR - - it_behaves_like 'allows facial match IAL only if sp is authorized', - Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_REQUIRED_ACR end end + it_behaves_like 'allows facial match IAL only if sp is authorized', + Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF + + it_behaves_like 'allows facial match IAL only if sp is authorized', + Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF + context 'with aal but not ial requested via acr_values' do let(:acr_values) { Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF } let(:vtr) { nil } @@ -473,39 +433,22 @@ end describe '#acr_values' do - let(:vtr) { nil } - let(:acr_value_list) do + let(:acr_values) do [ - Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::LOA1_AUTHN_CONTEXT_CLASSREF, - ] + 'http://idmanagement.gov/ns/assurance/loa/1', + 'http://idmanagement.gov/ns/assurance/aal/3', + 'fake_value', + ].join(' ') end - let(:acr_values) { acr_value_list.join(' ') } + let(:vtr) { nil } it 'is parsed into an array of valid ACR values' do - expect(form.acr_values).to eq acr_value_list - end - - context 'when an unknown acr value is included' do - let(:acr_value_list) do - [ - Saml::Idp::Constants::LOA1_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF, - ] - end - let(:acr_values) { (acr_value_list + ['fake-value']).join(' ') } - - it 'is parsed into an array of valid ACR values' do - expect(form.acr_values).to eq acr_value_list - end - end - - context 'when the only value is an unknown acr value' do - let(:acr_values) { 'fake_value' } - - it 'returns an empty array for acr_values' do - expect(form.acr_values).to eq([]) - end + expect(form.acr_values).to eq( + %w[ + http://idmanagement.gov/ns/assurance/loa/1 + http://idmanagement.gov/ns/assurance/aal/3 + ], + ) end end @@ -603,26 +546,6 @@ expect(requested_aal_value).to eq(phishing_resistant) end end - - context 'when no values are passed in' do - let(:acr_values) { '' } - - it 'returns the default AAL value' do - expect(form.requested_aal_value).to eq( - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, - ) - end - end - - context 'when only an unknown value is passed in' do - let(:acr_values) { 'fake-value' } - - it 'returns the default AAL value' do - expect(form.requested_aal_value).to eq( - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, - ) - end - end end end diff --git a/spec/services/attribute_asserter_spec.rb b/spec/services/attribute_asserter_spec.rb index 3903cbf66fb..e74d4328d20 100644 --- a/spec/services/attribute_asserter_spec.rb +++ b/spec/services/attribute_asserter_spec.rb @@ -107,20 +107,6 @@ it 'gets UUID from Service Provider' do expect(get_asserted_attribute(user, :uuid)).to eq user.last_identity.uuid end - - context 'when authn_context includes an unknown value' do - let(:authn_context) do - [ - ial_value, - 'unknown/authn/context', - ] - end - - it 'includes all requested attributes + uuid' do - expect(user.asserted_attributes.keys). - to eq(%i[uuid email phone first_name verified_at aal ial]) - end - end end context 'custom bundle includes dob' do diff --git a/spec/services/authn_context_resolver_spec.rb b/spec/services/authn_context_resolver_spec.rb index d3bb6b20570..60e4f4f6417 100644 --- a/spec/services/authn_context_resolver_spec.rb +++ b/spec/services/authn_context_resolver_spec.rb @@ -48,8 +48,8 @@ vtr = ['C2.Pb'] acr_values = [ - Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::IAL_VERIFIED_ACR, + 'http://idmanagement.gov/ns/assurance/aal/2', + 'http://idmanagement.gov/ns/assurance/ial/2', ].join(' ') result = AuthnContextResolver.new( @@ -152,8 +152,8 @@ context 'with no service provider' do it 'parses an ACR value into requirements' do acr_values = [ - Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + 'http://idmanagement.gov/ns/assurance/aal/2', + 'http://idmanagement.gov/ns/assurance/ial/1', ].join(' ') result = AuthnContextResolver.new( @@ -175,7 +175,7 @@ it 'properly parses an ACR value without an AAL ACR' do acr_values = [ - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + 'http://idmanagement.gov/ns/assurance/ial/1', ].join(' ') result = AuthnContextResolver.new( @@ -197,7 +197,7 @@ it 'properly parses an ACR value without an IAL ACR' do acr_values = [ - Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, + 'http://idmanagement.gov/ns/assurance/aal/2', ].join(' ') result = AuthnContextResolver.new( @@ -223,8 +223,8 @@ service_provider = build(:service_provider, default_aal: 2) acr_values = [ - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + 'http://idmanagement.gov/ns/assurance/aal/1', + 'http://idmanagement.gov/ns/assurance/ial/1', ].join(' ') result = AuthnContextResolver.new( @@ -241,7 +241,7 @@ service_provider = build(:service_provider, default_aal: 2) acr_values = [ - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + 'http://idmanagement.gov/ns/assurance/ial/1', ].join(' ') result = AuthnContextResolver.new( @@ -259,7 +259,7 @@ service_provider = build(:service_provider, default_aal: 3) acr_values = [ - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + 'http://idmanagement.gov/ns/assurance/ial/1', ].join(' ') result = AuthnContextResolver.new( @@ -295,10 +295,10 @@ let(:service_provider) { build(:service_provider, ial: 2) } subject do AuthnContextResolver.new( - user:, - service_provider:, + user: user, + service_provider: service_provider, vtr: nil, - acr_values:, + acr_values: acr_values, ) end @@ -307,8 +307,8 @@ context 'if IAL ACR value is present' do let(:acr_values) do [ - Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + 'http://idmanagement.gov/ns/assurance/ial/1', + 'http://idmanagement.gov/ns/assurance/aal/1', ].join(' ') end @@ -318,12 +318,12 @@ end end - context 'when multiple IAL ACR values are present' do + context 'if multiple IAL ACR values are present' do let(:acr_values) do [ - Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + 'http://idmanagement.gov/ns/assurance/ial/1', + 'http://idmanagement.gov/ns/assurance/ial/2', + 'http://idmanagement.gov/ns/assurance/aal/1', ].join(' ') end @@ -331,28 +331,12 @@ expect(result.identity_proofing?).to be true expect(result.aal2?).to be true end - - context 'when one of the acr values is unknown' do - let(:acr_values) do - [ - Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, - 'unknown-acr-value', - ].join(' ') - end - - it 'ignores the unknown value and uses the highest IAL ACR' do - expect(result.identity_proofing?).to eq(true) - expect(result.aal2?).to eq(true) - end - end end context 'if No IAL ACR is present' do let(:acr_values) do [ - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + 'http://idmanagement.gov/ns/assurance/aal/1', ].join(' ') end @@ -362,23 +346,12 @@ end end - context 'when the only ACR value is unknown' do - let(:acr_values) { 'unknown-acr-value' } - - it 'errors out as if there were no values' do - expect { result }.to raise_error Vot::Parser::ParseException - end - end - context 'if requesting facial match comparison' do - let(:bio_acr_value) do - Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF - end - + let(:bio_value) { 'required' } let(:acr_values) do [ - bio_acr_value, - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + "http://idmanagement.gov/ns/assurance/ial/2?bio=#{bio_value}", + 'http://idmanagement.gov/ns/assurance/aal/1', ].join(' ') end @@ -418,9 +391,7 @@ end context 'with facial match comparison is preferred' do - let(:bio_acr_value) do - Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF - end + let(:bio_value) { 'preferred' } context 'when the user is already verified' do context 'without facial match comparison' do @@ -507,7 +478,7 @@ context 'with no service provider' do it 'parses an ACR value into requirements' do acr_values = [ - Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, + 'http://idmanagement.gov/ns/assurance/aal/2', Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, ] @@ -554,7 +525,7 @@ it 'properly parses an ACR value without an IAL ACR' do acr_values = [ - Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, + 'http://idmanagement.gov/ns/assurance/aal/2', ] resolver = AuthnContextResolver.new( user: user, @@ -590,8 +561,8 @@ context 'if IAL ACR value is present' do let(:acr_values) do [ - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + 'http://idmanagement.gov/ns/assurance/ial/1', + 'http://idmanagement.gov/ns/assurance/aal/1', ] end @@ -601,12 +572,12 @@ end end - context 'when multiple IAL ACR values are present' do + context 'if multiple IAL ACR values are present' do let(:acr_values) do [ - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, - Saml::Idp::Constants::IAL_VERIFIED_ACR, - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + 'http://idmanagement.gov/ns/assurance/ial/1', + 'urn:acr.login.gov:verified', + 'http://idmanagement.gov/ns/assurance/aal/1', ] end @@ -614,28 +585,12 @@ expect(result.identity_proofing?).to be true expect(result.aal2?).to be true end - - context 'when one of the acr values is unknown' do - let(:acr_values) do - [ - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, - Saml::Idp::Constants::IAL_VERIFIED_ACR, - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, - 'unknown-acr-value', - ] - end - - it 'ignores the unknown value and uses the highest IAL ACR' do - expect(result.identity_proofing?).to eq(true) - expect(result.aal2?).to eq(true) - end - end end context 'if No IAL ACR is present' do let(:acr_values) do [ - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + 'http://idmanagement.gov/ns/assurance/aal/1', ] end @@ -661,14 +616,11 @@ end context 'if requesting facial match comparison' do - let(:bio_acr_value) do - Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_REQUIRED_ACR - end - + let(:bio_value) { 'required' } let(:acr_values) do [ - bio_acr_value, - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + "urn:acr.login.gov:verified-facial-match-#{bio_value}", + 'http://idmanagement.gov/ns/assurance/aal/1', ] end @@ -722,9 +674,7 @@ end context 'with facial match comparison is preferred' do - let(:bio_acr_value) do - Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_PREFERRED_ACR - end + let(:bio_value) { 'preferred' } context 'when the user is already verified' do context 'without facial match comparison' do diff --git a/spec/services/saml_request_validator_spec.rb b/spec/services/saml_request_validator_spec.rb index 73ceaedc9a2..c675f3a0b43 100644 --- a/spec/services/saml_request_validator_spec.rb +++ b/spec/services/saml_request_validator_spec.rb @@ -2,8 +2,7 @@ RSpec.describe SamlRequestValidator do describe '#call' do - let(:issuer) { 'http://localhost:3000' } - let(:sp) { ServiceProvider.find_by(issuer:) } + let(:sp) { ServiceProvider.find_by(issuer: 'http://localhost:3000') } let(:name_id_format) { Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT } let(:authn_context) { [Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF] } let(:comparison) { 'exact' } @@ -32,27 +31,15 @@ ).and_return( use_vot_in_sp_requests, ) - allow(IdentityConfig.store).to receive( - :allowed_biometric_ial_providers, - ).and_return([issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([issuer]) end context 'valid authn context and sp and authorized nameID format' do - [ - Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, - ].each do |ial_value| - let(:authn_context) { [ial_value] } - it 'returns FormResponse with success: true' do - expect(response.to_h).to include( - success: true, - errors: {}, - **extra, - ) - end + it 'returns FormResponse with success: true' do + expect(response.to_h).to include( + success: true, + errors: {}, + **extra, + ) end context 'ialmax authncontext and ialmax provider' do @@ -72,17 +59,6 @@ end end - context 'no authn context and valid sp and authorized nameID format' do - let(:authn_context) { [] } - it 'returns FormResponse with success: true' do - expect(response.to_h).to include( - success: true, - errors: {}, - **extra, - ) - end - end - context 'valid authn context and invalid sp and authorized nameID format' do let(:sp) { ServiceProvider.find_by(issuer: 'foo') } @@ -204,8 +180,8 @@ end end - context 'unknown context and valid sp and authorized nameID format' do - context 'only the unknown authn_context is requested' do + context 'invalid authn context and valid sp and authorized nameID format' do + context 'unknown auth context' do let(:authn_context) { ['IAL1'] } it 'returns FormResponse with success: false' do @@ -220,39 +196,22 @@ **extra, ) end - - context 'unknown authn_context requested along with a valid one' do - let(:authn_context) { ['IAL1', Saml::Idp::Constants::IAL_AUTH_ONLY_ACR] } - - it 'returns FormResponse with success: true' do - expect(response.to_h).to include( - success: true, - errors: {}, - **extra, - ) - end - end end context 'authn context is ial2 when sp is ial 1' do - [ - Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::IAL_VERIFIED_ACR, - ].each do |ial_value| - let(:authn_context) { [ial_value] } - - it 'returns FormResponse with success: false' do - errors = { - authn_context: [t('errors.messages.unauthorized_authn_context')], - } - - expect(response.to_h).to include( - success: false, - errors: errors, - error_details: hash_including(*errors.keys), - **extra, - ) - end + let(:authn_context) { [Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF] } + + it 'returns FormResponse with success: false' do + errors = { + authn_context: [t('errors.messages.unauthorized_authn_context')], + } + + expect(response.to_h).to include( + success: false, + errors: errors, + error_details: hash_including(*errors.keys), + **extra, + ) end end @@ -278,8 +237,9 @@ context "when the IAL requested is #{facial_match_ial}" do context 'when the service provider is allowed to use facial match ials' do + let(:sp) { create(:service_provider, :idv) } + before do - sp.update(ial: 2) allow_any_instance_of(ServiceProvider).to receive(:facial_match_ial_allowed?). and_return(true) end @@ -321,19 +281,14 @@ it_behaves_like 'allows facial match IAL only if sp is authorized', Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF - it_behaves_like 'allows facial match IAL only if sp is authorized', - Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_PREFERRED_ACR - - it_behaves_like 'allows facial match IAL only if sp is authorized', - Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_REQUIRED_ACR - shared_examples 'allows semantic IAL only if sp is authorized' do |semantic_ial| let(:authn_context) { [semantic_ial] } context "when the IAL requested is #{semantic_ial}" do context 'when the service provider is allowed to use semantic ials' do + let(:sp) { create(:service_provider, :idv) } + before do - sp.update(ial: 2) allow_any_instance_of(ServiceProvider). to receive(:semantic_authn_contexts_allowed?). and_return(true) diff --git a/spec/services/vot/parser_spec.rb b/spec/services/vot/parser_spec.rb index 6065776e08b..43e301f3ade 100644 --- a/spec/services/vot/parser_spec.rb +++ b/spec/services/vot/parser_spec.rb @@ -97,94 +97,22 @@ end context 'when input includes unrecognized components' do - let(:acr_values) { 'unknown-acr-value' } - - context 'only an unknown acr_value is passed in' do - it 'raises an exception' do - expect { Vot::Parser.new(acr_values:).parse }.to raise_exception( - Vot::Parser::ParseException, - 'VoT parser called without VoT or ACR values', - ) - end - - context 'when a known and valid acr_value is passed in as well' do - let(:acr_values) do - [ - 'unknown-acr-value', - Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, - ].join(' ') - end - - it 'parses ACR values to component values' do - result = Vot::Parser.new(acr_values:).parse - - expect(result.component_values.map(&:name).join(' ')).to eq( - Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, - ) - expect(result.aal2?).to eq(false) - expect(result.phishing_resistant?).to eq(false) - expect(result.hspd12?).to eq(false) - expect(result.identity_proofing?).to eq(false) - expect(result.facial_match?).to eq(false) - expect(result.ialmax?).to eq(false) - expect(result.enhanced_ipp?).to eq(false) - end - - context 'with semantic acr_values' do - let(:acr_values) do - [ - 'unknown-acr-value', - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, - ].join(' ') - end - - it 'parses ACR values to component values' do - result = Vot::Parser.new(acr_values:).parse - - expect(result.component_values.map(&:name).join(' ')).to eq( - Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, - ) - expect(result.aal2?).to eq(false) - expect(result.phishing_resistant?).to eq(false) - expect(result.hspd12?).to eq(false) - expect(result.identity_proofing?).to eq(false) - expect(result.facial_match?).to eq(false) - expect(result.ialmax?).to eq(false) - expect(result.enhanced_ipp?).to eq(false) - end - end - end + let(:acr_values) { 'i-am-not-an-acr-value' } + it 'raises an exception' do + expect { Vot::Parser.new(acr_values:).parse }.to raise_exception( + Vot::Parser::UnsupportedComponentsException, + /'i-am-not-an-acr-value'$/, + ) end context 'with vectors of trust' do - context 'only an unknown VoT is passed in' do - it 'raises an exception' do - vector_of_trust = 'Xx' - - expect { Vot::Parser.new(vector_of_trust:).parse }.to raise_exception( - Vot::Parser::ParseException, - 'VoT parser called without VoT or ACR values', - ) - end - end + it 'raises an exception' do + vector_of_trust = 'C1.C2.Xx' - context 'along with a known vector' do - it 'parses the vector' do - vector_of_trust = 'C1.C2.Xx' - - result = Vot::Parser.new(vector_of_trust:).parse - - expect(result.component_values.map(&:name).join(' ')).to eq( - 'C1 C2', - ) - expect(result.aal2?).to eq(true) - expect(result.phishing_resistant?).to eq(false) - expect(result.hspd12?).to eq(false) - expect(result.identity_proofing?).to eq(false) - expect(result.facial_match?).to eq(false) - expect(result.ialmax?).to eq(false) - expect(result.enhanced_ipp?).to eq(false) - end + expect { Vot::Parser.new(vector_of_trust:).parse }.to raise_exception( + Vot::Parser::UnsupportedComponentsException, + /'Xx'$/, + ) end end end