diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index 8f8c919592c..2a26f78d205 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -260,8 +260,7 @@ def scopes def validate_privileges if (ial2_requested? && !ial_context.ial2_service_provider?) || - (ial_context.ialmax_requested? && - !IdentityConfig.store.allowed_ialmax_providers.include?(client_id)) + (ial_context.ialmax_requested? && !ial_context.ial2_service_provider?) errors.add( :acr_values, t('openid_connect.authorization.errors.no_auth'), type: :no_auth diff --git a/app/services/saml_request_validator.rb b/app/services/saml_request_validator.rb index 21ddd0ea7db..75cc2b6707a 100644 --- a/app/services/saml_request_validator.rb +++ b/app/services/saml_request_validator.rb @@ -39,9 +39,7 @@ def authorized_service_provider def authorized_authn_context if !valid_authn_context? || - (ial2_context_requested? && service_provider&.ial != 2) || - (ial_max_requested? && - !IdentityConfig.store.allowed_ialmax_providers.include?(service_provider&.issuer)) + (ial2_context_requested? && service_provider&.ial != 2) errors.add(:authn_context, :unauthorized_authn_context, type: :unauthorized_authn_context) end end @@ -73,10 +71,6 @@ def ial2_context_requested? end end - def ial_max_requested? - Array(authn_context).include?(Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF) - end - def authorized_email_nameid_format return unless email_nameid_format? return if service_provider&.email_nameid_format_allowed diff --git a/config/application.yml.default b/config/application.yml.default index c428faaddfa..bcb1e904f7b 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -22,7 +22,6 @@ aamva_supported_jurisdictions: '["AL","AR","AZ","CO","CT","DC","DE","FL","GA","H aamva_verification_request_timeout: 5.0 aamva_verification_url: https://example.org:12345/verification/url all_redirect_uris_cache_duration_minutes: 2 -allowed_ialmax_providers: '[]' account_reset_token_valid_for_days: 1 account_reset_wait_period_days: 1 account_suspended_support_code: EFGHI diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 27a1d873a69..cc262c5fef7 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -95,7 +95,6 @@ def self.build_store(config_map) config.add(:aamva_verification_request_timeout, type: :float) config.add(:aamva_verification_url) config.add(:all_redirect_uris_cache_duration_minutes, type: :integer) - config.add(:allowed_ialmax_providers, type: :json) config.add(:account_reset_token_valid_for_days, type: :integer) config.add(:account_reset_wait_period_days, type: :integer) config.add(:account_suspended_support_code, type: :string) diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index a0ea04d1fbb..e16b0483de3 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -182,166 +182,161 @@ end context 'with ialmax requested' do - context 'provider is on the ialmax allow list' do - before do - params[:acr_values] = Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF - allow(IdentityConfig.store).to receive(:allowed_ialmax_providers) { [client_id] } - end + before { params[:acr_values] = Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF } - context 'account is already verified' do - let(:user) do - create( - :profile, :active, :verified, proofing_components: { liveness_check: true } - ).user - end + context 'account is already verified' do + let(:user) do + create( + :profile, :active, :verified, proofing_components: { liveness_check: true } + ).user + end - it 'redirects to the redirect_uri immediately when pii is unlocked' do - IdentityLinker.new(user, service_provider).link_identity(ial: 3) - user.identities.last.update!( - verified_attributes: %w[given_name family_name birthdate verified_at], - ) - allow(controller).to receive(:pii_requested_but_locked?).and_return(false) - action + it 'redirects to the redirect_uri immediately when pii is unlocked' do + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action - expect(response).to redirect_to(/^#{params[:redirect_uri]}/) - end + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end - it 'redirects to the password capture url when pii is locked' do - IdentityLinker.new(user, service_provider).link_identity(ial: 3) - user.identities.last.update!( - verified_attributes: %w[given_name family_name birthdate verified_at], - ) - allow(controller).to receive(:pii_requested_but_locked?).and_return(true) - action + it 'redirects to the password capture url when pii is locked' do + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(true) + action - expect(response).to redirect_to(capture_password_url) - end + expect(response).to redirect_to(capture_password_url) + end - it 'tracks IAL2 authentication event' do - stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: true, - client_id: client_id, - prompt: 'select_account', - referer: nil, - user_sp_authorized: true, - allow_prompt_login: true, - errors: {}, - unauthorized_scope: false, - user_fully_authenticated: true, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', - scope: 'openid profile', - code_digest: kind_of(String)) - expect(@analytics).to receive(:track_event). - with( - 'SP redirect initiated', - ial: 0, - billed_ial: 2, - ) - - IdentityLinker.new(user, service_provider).link_identity(ial: 2) - user.identities.last.update!( - verified_attributes: %w[given_name family_name birthdate verified_at], + it 'tracks IAL2 authentication event' do + stub_analytics + expect(@analytics).to receive(:track_event). + with('OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + user_sp_authorized: true, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + scope: 'openid profile', + code_digest: kind_of(String)) + expect(@analytics).to receive(:track_event). + with( + 'SP redirect initiated', + ial: 0, + billed_ial: 2, ) - allow(controller).to receive(:pii_requested_but_locked?).and_return(false) - action - sp_return_log = SpReturnLog.find_by(issuer: client_id) - expect(sp_return_log.ial).to eq(2) - end + IdentityLinker.new(user, service_provider).link_identity(ial: 2) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + action + + sp_return_log = SpReturnLog.find_by(issuer: client_id) + expect(sp_return_log.ial).to eq(2) end + end - context 'account is not already verified' do - it 'redirects to the redirect_uri immediately without proofing' do - IdentityLinker.new(user, service_provider).link_identity(ial: 1) - user.identities.last.update!( - verified_attributes: %w[given_name family_name birthdate verified_at], - ) + context 'account is not already verified' do + it 'redirects to the redirect_uri immediately without proofing' do + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) - action - expect(response).to redirect_to(/^#{params[:redirect_uri]}/) - end + action + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end - it 'tracks IAL1 authentication event' do - stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: true, - client_id: client_id, - prompt: 'select_account', - referer: nil, - user_sp_authorized: true, - allow_prompt_login: true, - errors: {}, - unauthorized_scope: false, - user_fully_authenticated: true, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', - scope: 'openid profile', - code_digest: kind_of(String)) - expect(@analytics).to receive(:track_event). - with( - 'SP redirect initiated', - ial: 0, - billed_ial: 1, - ) - - IdentityLinker.new(user, service_provider).link_identity(ial: 1) - user.identities.last.update!( - verified_attributes: %w[given_name family_name birthdate verified_at], + it 'tracks IAL1 authentication event' do + stub_analytics + expect(@analytics).to receive(:track_event). + with('OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + user_sp_authorized: true, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + scope: 'openid profile', + code_digest: kind_of(String)) + expect(@analytics).to receive(:track_event). + with( + 'SP redirect initiated', + ial: 0, + billed_ial: 1, ) - action - sp_return_log = SpReturnLog.find_by(issuer: client_id) - expect(sp_return_log.ial).to eq(1) - end + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + action + + sp_return_log = SpReturnLog.find_by(issuer: client_id) + expect(sp_return_log.ial).to eq(1) end + end - context 'profile is reset' do - let(:user) { create(:profile, :verified, :password_reset).user } + context 'profile is reset' do + let(:user) { create(:profile, :verified, :password_reset).user } - it 'redirects to the redirect_uri immediately without proofing' do - IdentityLinker.new(user, service_provider).link_identity(ial: 1) - user.identities.last.update!( - verified_attributes: %w[given_name family_name birthdate verified_at], - ) + it 'redirects to the redirect_uri immediately without proofing' do + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) - action - expect(response).to redirect_to(/^#{params[:redirect_uri]}/) - end + action + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + end - it 'tracks IAL1 authentication event' do - stub_analytics - expect(@analytics).to receive(:track_event). - with('OpenID Connect: authorization request', - success: true, - client_id: client_id, - prompt: 'select_account', - referer: nil, - user_sp_authorized: true, - allow_prompt_login: true, - errors: {}, - unauthorized_scope: false, - user_fully_authenticated: true, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', - scope: 'openid profile', - code_digest: kind_of(String)) - expect(@analytics).to receive(:track_event). - with( - 'SP redirect initiated', - ial: 0, - billed_ial: 1, - ) - - IdentityLinker.new(user, service_provider).link_identity(ial: 1) - user.identities.last.update!( - verified_attributes: %w[given_name family_name birthdate verified_at], + it 'tracks IAL1 authentication event' do + stub_analytics + expect(@analytics).to receive(:track_event). + with('OpenID Connect: authorization request', + success: true, + client_id: client_id, + prompt: 'select_account', + referer: nil, + user_sp_authorized: true, + allow_prompt_login: true, + errors: {}, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: 'http://idmanagement.gov/ns/assurance/ial/0', + scope: 'openid profile', + code_digest: kind_of(String)) + expect(@analytics).to receive(:track_event). + with( + 'SP redirect initiated', + ial: 0, + billed_ial: 1, ) - action - sp_return_log = SpReturnLog.find_by(issuer: client_id) - expect(sp_return_log.ial).to eq(1) - end + IdentityLinker.new(user, service_provider).link_identity(ial: 1) + user.identities.last.update!( + verified_attributes: %w[given_name family_name birthdate verified_at], + ) + action + + sp_return_log = SpReturnLog.find_by(issuer: client_id) + expect(sp_return_log.ial).to eq(1) end end end @@ -470,24 +465,6 @@ end end - context 'ialmax requested when service provider is not in allowlist' do - before do - params[:acr_values] = Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF - end - - it 'redirect the user' do - action - - expect(response).to redirect_to(/^#{params[:redirect_uri]}/) - - redirect_params = UriService.params(response.location) - - expect(redirect_params[:error]).to eq('invalid_request') - expect(redirect_params[:error_description]).to be_present - expect(redirect_params[:state]).to eq(params[:state]) - end - end - it 'redirects to SP landing page with the request_id in the params' do action sp_request_id = ServiceProviderRequestProxy.last.uuid diff --git a/spec/features/reports/authorization_count_spec.rb b/spec/features/reports/authorization_count_spec.rb index a82067f5d68..514067e6797 100644 --- a/spec/features/reports/authorization_count_spec.rb +++ b/spec/features/reports/authorization_count_spec.rb @@ -76,26 +76,10 @@ def visit_idp_from_ial2_saml_sp(issuer:) expect_ial1_and_ial2_count(client_id_1) end - context 'the service provider is on the ialmax approved list' do - before do - allow(IdentityConfig.store).to receive(:allowed_ialmax_providers) { [client_id_1] } - end - - it 'counts IAL1 auth when ial max is requested' do - visit_idp_from_ial_max_oidc_sp(client_id: client_id_1) - click_agree_and_continue - - expect_ial1_count_only(client_id_1) - end - end - - context 'the service provider is not on the ialmax approved list' do - it 'does not count the auth attempt' do - visit_idp_from_ial_max_oidc_sp(client_id: client_id_1) - - expect(page).not_to have_content t('sign_up.agree_and_continue') - expect_no_counts(issuer_1) - end + it 'counts IAL1 auth when ial max is requested' do + visit_idp_from_ial_max_oidc_sp(client_id: client_id_1) + click_agree_and_continue + expect_ial1_count_only(client_id_1) end end @@ -127,57 +111,26 @@ def visit_idp_from_ial2_saml_sp(issuer:) end # rubocop:disable Layout/LineLength - context 'when ialmax is requested' do - context 'provider is on the ialmax allow list' do - before do - allow(IdentityConfig.store).to receive(:allowed_ialmax_providers) { [issuer_1] } - end - - it 'counts IAL1 auth when ial max is requested' do - visit_saml_authn_request_url( - overrides: { - issuer: issuer_1, - name_identifier_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, - authn_context: [ - Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name:last_name email, ssn", - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", - ], - security: { - embed_sign: false, - }, - }, - ) - click_agree_and_continue - click_submit_default - - expect_ial1_count_only(issuer_1) - end - end - - context 'provider is not on the ialmax allow list' do - it 'does not count the auth attempt' do - visit_saml_authn_request_url( - overrides: { - issuer: issuer_1, - name_identifier_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, - authn_context: [ - Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name:last_name email, ssn", - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", - ], - security: { - embed_sign: false, - }, - }, - ) - - expect(page).not_to have_content t('sign_up.agree_and_continue') - expect_no_counts(issuer_1) - end - end - # rubocop:enable Layout/LineLength + it 'counts IAL1 auth when ial max is requested' do + visit_saml_authn_request_url( + overrides: { + issuer: issuer_1, + name_identifier_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, + authn_context: [ + Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name:last_name email, ssn", + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", + ], + security: { + embed_sign: false, + }, + }, + ) + click_agree_and_continue + click_submit_default + expect_ial1_count_only(issuer_1) end + # rubocop:enable Layout/LineLength end end @@ -239,28 +192,10 @@ def visit_idp_from_ial2_saml_sp(issuer:) expect_ial1_count_only(client_id_2) end - context 'ialmax is requested' do - context 'provider is on the ialmax allowlist' do - before do - allow(IdentityConfig.store).to receive(:allowed_ialmax_providers) { [client_id_1] } - end - - it 'counts IAL2 auth when ial max is requested' do - visit_idp_from_ial_max_oidc_sp(client_id: client_id_1) - click_agree_and_continue - - expect_ial2_count_only(client_id_1) - end - end - - context 'provider is not on the ialmax allowlist' do - it 'does not count the auth attempt' do - visit_idp_from_ial_max_oidc_sp(client_id: client_id_1) - - expect(page).not_to have_content t('sign_up.agree_and_continue') - expect_no_counts(client_id_1) - end - end + it 'counts IAL2 auth when ial max is requested' do + visit_idp_from_ial_max_oidc_sp(client_id: client_id_1) + click_agree_and_continue + expect_ial2_count_only(client_id_1) end end @@ -325,56 +260,25 @@ def visit_idp_from_ial2_saml_sp(issuer:) end # rubocop:disable Layout/LineLength - context 'ialmax is requested' do - context 'provider is on ialmax allow list' do - before do - allow(IdentityConfig.store).to receive(:allowed_ialmax_providers) { [issuer_1] } - end - - it 'counts IAL2 auth when ial max is requested' do - visit_saml_authn_request_url( - overrides: { - issuer: issuer_1, - name_identifier_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, - authn_context: [ - Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name:last_name email, ssn", - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", - ], - security: { - embed_sign: false, - }, - }, - ) - click_agree_and_continue - click_submit_default - expect_ial2_count_only(issuer_1) - end - end - - context 'provider is not on ialmax allow list' do - it 'does not count the auth attempt' do - visit_saml_authn_request_url( - overrides: { - issuer: issuer_1, - name_identifier_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, - authn_context: [ - Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name:last_name email, ssn", - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", - ], - security: { - embed_sign: false, - }, - }, - ) - - expect(page).not_to have_content t('sign_up.agree_and_continue') - expect_no_counts(issuer_1) - end - end + it 'counts IAL2 auth when ial max is requested' do + visit_saml_authn_request_url( + overrides: { + issuer: issuer_1, + name_identifier_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, + authn_context: [ + Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name:last_name email, ssn", + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", + ], + security: { + embed_sign: false, + }, + }, + ) + click_agree_and_continue + click_submit_default + expect_ial2_count_only(issuer_1) end - # rubocop:enable Layout/LineLength end end @@ -400,13 +304,6 @@ def expect_ial1_and_ial2_count(issuer) expect(ial2_return_logs.count).to eq(1) end - def expect_no_counts(issuer) - ial1_return_logs = SpReturnLog.where(issuer: issuer, billable: true, ial: 1) - ial2_return_logs = SpReturnLog.where(issuer: issuer, billable: true, ial: 2) - expect(ial1_return_logs.count).to eq(0) - expect(ial2_return_logs.count).to eq(0) - end - def reset_monthly_auth_count_and_login(user) SpReturnLog.delete_all visit api_saml_logout_path(path_year: SamlAuthHelper::PATH_YEAR) diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index e4cba75601c..84d902ce34a 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -15,19 +15,10 @@ expect(page).to_not have_content t('devise.registrations.start.accordion') end - context 'service provider is on the ialmax allow list' do - before do - allow(IdentityConfig.store).to receive(:allowed_ialmax_providers) { - ['urn:gov:gsa:openidconnect:sp:server'] - } - end + scenario 'user signs in as ialmax and does not see ial2 help text' do + visit_idp_from_oidc_sp_with_ialmax - scenario 'user signs in as ialmax and does not see ial2 help text' do - visit_idp_from_oidc_sp_with_ialmax - - expect(page).to_not have_content t('devise.registrations.start.accordion') - expect(page).to_not have_content 'The page you were looking for doesn’t exist' - end + expect(page).to_not have_content t('devise.registrations.start.accordion') end scenario 'user signs in as ial2 and does see ial2 help text' do @@ -846,153 +837,91 @@ end context 'oidc sp requests ialmax' do - context 'the service_provider is on the allow list' do - before do - allow(IdentityConfig.store).to receive(:allowed_ialmax_providers) { - ['urn:gov:gsa:openidconnect:sp:server'] - } - end - - it 'returns ial1 info for a non-verified user' do - user = create(:user, :fully_registered) - visit_idp_from_oidc_sp_with_ialmax - fill_in_credentials_and_submit(user.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default - - expect(current_path).to eq sign_up_completed_path - expect(page).to have_content(user.email) - - click_agree_and_continue + it 'returns ial1 info for a non-verified user' do + user = create(:user, :fully_registered) + visit_idp_from_oidc_sp_with_ialmax + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default - expect(current_url).to start_with('http://localhost:7654/auth/result') - end + expect(current_path).to eq sign_up_completed_path + expect(page).to have_content(user.email) - it 'returns ial2 info for a verified user' do - user = create( - :profile, :active, :verified, - pii: { first_name: 'John', ssn: '111223333' } - ).user - visit_idp_from_oidc_sp_with_ialmax - fill_in_credentials_and_submit(user.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default + click_agree_and_continue - expect(current_path).to eq sign_up_completed_path - expect(page).to have_content('1**-**-***3') + expect(current_url).to start_with('http://localhost:7654/auth/result') + end - click_agree_and_continue + it 'returns ial2 info for a verified user' do + user = create( + :profile, :active, :verified, + pii: { first_name: 'John', ssn: '111223333' } + ).user + visit_idp_from_oidc_sp_with_ialmax + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default - expect(current_url).to start_with('http://localhost:7654/auth/result') - end - end + expect(current_path).to eq sign_up_completed_path + expect(page).to have_content('1**-**-***3') - context 'the service provider is not on the allow list' do - it 'returns an error' do - create(:user, :fully_registered) - visit_idp_from_oidc_sp_with_ialmax + click_agree_and_continue - expect(page).to have_content 'The page you were looking for doesn’t exist' - end + expect(current_url).to start_with('http://localhost:7654/auth/result') end end context 'saml sp requests ialmax' do - context 'the service provider is on the allow list' do - before do - allow(IdentityConfig.store).to receive(:allowed_ialmax_providers) { [sp1_issuer] } - end - - it 'returns ial1 info for a non-verified user' do - user = create(:user, :fully_registered) - visit_saml_authn_request_url( - overrides: { - issuer: sp1_issuer, - authn_context: [ - Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF + - 'first_name:last_name email, ssn', - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", - ], - }, - ) - fill_in_credentials_and_submit(user.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default_twice - - expect(current_path).to eq sign_up_completed_path - expect(page).to have_content(user.email) - - click_agree_and_continue + it 'returns ial1 info for a non-verified user' do + user = create(:user, :fully_registered) + visit_saml_authn_request_url( + overrides: { + issuer: sp1_issuer, + authn_context: [ + Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name:last_name email, ssn", + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", + ], + }, + ) + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default_twice - expect(current_url).to eq complete_saml_url - end + expect(current_path).to eq sign_up_completed_path + expect(page).to have_content(user.email) - it 'returns ial2 info for a verified user' do - user = create( - :profile, :active, :verified, - pii: { first_name: 'John', ssn: '111223333' } - ).user - visit_saml_authn_request_url( - overrides: { - issuer: sp1_issuer, - authn_context: [ - Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF + - 'first_name:last_name email, ssn', - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", - ], - }, - ) - fill_in_credentials_and_submit(user.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default - click_submit_default + click_agree_and_continue - expect(current_path).to eq sign_up_completed_path - expect(page).to have_content('1**-**-***3') + expect(current_url).to eq complete_saml_url + end - click_agree_and_continue + it 'returns ial2 info for a verified user' do + user = create( + :profile, :active, :verified, + pii: { first_name: 'John', ssn: '111223333' } + ).user + visit_saml_authn_request_url( + overrides: { + issuer: sp1_issuer, + authn_context: [ + Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}first_name:last_name email, ssn", + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", + ], + }, + ) + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + click_submit_default - expect(current_url).to eq complete_saml_url - end - end + expect(current_path).to eq sign_up_completed_path + expect(page).to have_content('1**-**-***3') - context 'the service provider is not on the allow list' do - it 'redirects to an error page for ial1 user' do - create(:user, :fully_registered) - visit_saml_authn_request_url( - overrides: { - issuer: sp1_issuer, - authn_context: [ - Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF + - 'first_name:last_name email, ssn', - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", - ], - }, - ) - expect(page).to_not have_content 'The page you were looking for doesn’t exist' - end + click_agree_and_continue - it 'redirects to an error page for ial2 user' do - create( - :profile, :active, :verified, - pii: { first_name: 'John', ssn: '111223333' } - ).user - visit_saml_authn_request_url( - overrides: { - issuer: sp1_issuer, - authn_context: [ - Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, - Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF + - 'first_name:last_name email, ssn', - "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}phone", - ], - }, - ) - expect(page).to_not have_content 'The page you were looking for doesn’t exist' - end + expect(current_url).to eq complete_saml_url end end diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 8362be9f8aa..6e5e1231147 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -119,29 +119,6 @@ end end - context 'with ialmax requested' do - let(:acr_values) { Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF } - - context 'with a service provider not in the allow list' do - it 'has errors' do - expect(valid?).to eq false - expect(form.errors[:acr_values]). - to include(t('openid_connect.authorization.errors.no_auth')) - end - end - - context 'with a service provider on the allow list' do - before do - expect(IdentityConfig.store).to receive(:allowed_ialmax_providers) { [client_id] } - end - - it 'has no errors' do - expect(valid?).to eq true - expect(form.errors).to be_blank - end - end - end - context 'with aal but not ial requested via acr_values' do let(:acr_values) { Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF } it 'has errors' do diff --git a/spec/services/saml_request_validator_spec.rb b/spec/services/saml_request_validator_spec.rb index ee0788b60a0..7fadb420fdb 100644 --- a/spec/services/saml_request_validator_spec.rb +++ b/spec/services/saml_request_validator_spec.rb @@ -2,59 +2,53 @@ RSpec.describe SamlRequestValidator do describe '#call' do - 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' } - let(:extra) do - { - authn_context: authn_context, - service_provider: sp&.issuer, - nameid_format: name_id_format, - authn_context_comparison: comparison, - } - end - - let(:response) do - SamlRequestValidator.new.call( - service_provider: sp, - authn_context: authn_context, - nameid_format: name_id_format, - ) - end - context 'valid authn context and sp and authorized nameID format' do it 'returns FormResponse with success: true' do + sp = ServiceProvider.find_by(issuer: 'http://localhost:3000') + authn_context = [Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF] + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT + extra = { + authn_context: authn_context, + service_provider: sp.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'exact', + } + + response = SamlRequestValidator.new.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, + ) + expect(response.to_h).to include( success: true, errors: {}, **extra, ) end - - context 'ialmax authncontext and ialmax provider' do - let(:authn_context) { [Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF] } - before do - expect(IdentityConfig.store).to receive(:allowed_ialmax_providers) { [sp.issuer] } - end - - it 'returns FormResponse with success: true' do - expect(response.to_h).to include( - success: true, - errors: {}, - **extra, - ) - end - end end context 'valid authn context and invalid sp and authorized nameID format' do - let(:sp) { ServiceProvider.find_by(issuer: 'foo') } it 'returns FormResponse with success: false' do + sp = ServiceProvider.find_by(issuer: 'foo') + authn_context = [Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF] + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT + extra = { + authn_context: authn_context, + service_provider: sp&.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'exact', + } errors = { service_provider: [t('errors.messages.unauthorized_service_provider')], } + response = SamlRequestValidator.new.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, + ) + expect(response.to_h).to include( success: false, errors: errors, @@ -65,12 +59,26 @@ end context 'valid authn context and unauthorized nameid format' do - let(:name_id_format) { Saml::Idp::Constants::NAME_ID_FORMAT_EMAIL } it 'returns FormResponse with success: false' do + sp = ServiceProvider.find_by(issuer: 'http://localhost:3000') + authn_context = [Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF] + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_EMAIL + extra = { + authn_context: authn_context, + service_provider: sp.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'exact', + } errors = { nameid_format: [t('errors.messages.unauthorized_nameid_format')], } + response = SamlRequestValidator.new.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, + ) + expect(response.to_h).to include( success: false, errors: errors, @@ -81,10 +89,24 @@ end context 'valid authn context and authorized email nameid format for SP' do - let(:sp) { ServiceProvider.find_by(issuer: 'https://rp1.serviceprovider.com/auth/saml/metadata') } - let(:name_id_format) { Saml::Idp::Constants::NAME_ID_FORMAT_EMAIL } - before { sp.update!(email_nameid_format_allowed: true) } it 'returns FormResponse with success: true' do + sp = ServiceProvider.find_by(issuer: 'https://rp1.serviceprovider.com/auth/saml/metadata') + sp.update!(email_nameid_format_allowed: true) + authn_context = [Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF] + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_EMAIL + extra = { + authn_context: authn_context, + service_provider: sp.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'exact', + } + + response = SamlRequestValidator.new.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, + ) + expect(response.to_h).to include( success: true, errors: {}, @@ -92,24 +114,47 @@ ) end - context 'ial2 authn context and ial2 sp' do - let(:authn_context) { [Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF] } - before { sp.update!(ial: 2) } - it 'returns FormResponse with success: true for ial2 on ial:2 sp' do - expect(response.to_h).to include( - success: true, - errors: {}, - **extra, - ) - end + it 'returns FormResponse with success: true for ial2 on ial:2 sp' do + sp = ServiceProvider.find_by(issuer: 'https://rp1.serviceprovider.com/auth/saml/metadata') + sp.update!(email_nameid_format_allowed: true) + sp.ial = 2 + authn_context = [Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF] + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_EMAIL + extra = { + authn_context: authn_context, + service_provider: sp.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'exact', + } + + response = SamlRequestValidator.new.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, + ) + + expect(response.to_h).to include( + success: true, + errors: {}, + **extra, + ) end end context 'unsupported authn context with step up and valid sp and nameID format' do - Saml::Idp::Constants::PASSWORD_AUTHN_CONTEXT_CLASSREFS.each do |password_context| - let(:authn_context) { [password_context] } - let(:comparison) { 'minimum' } - it 'returns a FormResponse with success: true for Comparison=minimum' do + it 'returns a FormResponse with success: true for Comparison=minimum' do + Saml::Idp::Constants::PASSWORD_AUTHN_CONTEXT_CLASSREFS.each do |password_context| + sp = ServiceProvider.find_by(issuer: 'http://localhost:3000') + authn_context = [password_context] + comparison = 'minimum' + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT + extra = { + authn_context: authn_context, + service_provider: sp.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'minimum', + } + response = SamlRequestValidator.new.call( service_provider: sp, authn_context: authn_context, @@ -125,10 +170,19 @@ end end - Saml::Idp::Constants::PASSWORD_AUTHN_CONTEXT_CLASSREFS.each do |password_context| - let(:authn_context) { [password_context] } - let(:comparison) { 'better' } - it 'returns a FormResponse with success: true for Comparison=better' do + it 'returns a FormResponse with success: true for Comparison=better' do + Saml::Idp::Constants::PASSWORD_AUTHN_CONTEXT_CLASSREFS.each do |password_context| + sp = ServiceProvider.find_by(issuer: 'http://localhost:3000') + authn_context = [password_context] + comparison = 'better' + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT + extra = { + authn_context: authn_context, + service_provider: sp.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'better', + } + response = SamlRequestValidator.new.call( service_provider: sp, authn_context: authn_context, @@ -146,30 +200,26 @@ end context 'unsupported authn context without step up and valid sp and nameID format' do - Saml::Idp::Constants::PASSWORD_AUTHN_CONTEXT_CLASSREFS.each do |password_context| - let(:authn_context) { [password_context] } - it 'returns FormResponse with success: false for unknown authn context' do + it 'returns FormResponse with success: false for unknown authn context' do + Saml::Idp::Constants::PASSWORD_AUTHN_CONTEXT_CLASSREFS.each do |password_context| + sp = ServiceProvider.find_by(issuer: 'http://localhost:3000') + authn_context = [password_context] + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT + extra = { + authn_context: authn_context, + service_provider: sp.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'exact', + } 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, + response = SamlRequestValidator.new.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, ) - end - end - end - - 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 - errors = { - authn_context: [t('errors.messages.unauthorized_authn_context')], - } expect(response.to_h).to include( success: false, @@ -179,52 +229,89 @@ ) end end + end - context 'authn context is ial2 when sp is ial 1' do - let(:authn_context) { [Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF] } + context 'invalid authn context and valid sp and authorized nameID format' do + it 'returns FormResponse with success: false for unknown authn context' do + sp = ServiceProvider.find_by(issuer: 'http://localhost:3000') + authn_context = ['IAL1'] + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT + extra = { + authn_context: authn_context, + service_provider: sp.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'exact', + } + errors = { + authn_context: [t('errors.messages.unauthorized_authn_context')], + } - it 'returns FormResponse with success: false' do - errors = { - authn_context: [t('errors.messages.unauthorized_authn_context')], - } + response = SamlRequestValidator.new.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, + ) - expect(response.to_h).to include( - success: false, - errors: errors, - error_details: hash_including(*errors.keys), - **extra, - ) - end + expect(response.to_h).to include( + success: false, + errors: errors, + error_details: hash_including(*errors.keys), + **extra, + ) end - context 'authn context is ialmax when sp is not included' do - let(:authn_context) { [Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF] } + it 'returns FormResponse with success: false for ial2 on an ial:1 sp' do + sp = ServiceProvider.find_by(issuer: 'http://localhost:3000') + sp.ial = 1 + authn_context = [Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF] + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT + extra = { + authn_context: authn_context, + service_provider: sp.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'exact', + } + errors = { + authn_context: [t('errors.messages.unauthorized_authn_context')], + } - it 'returns FormResponse with success: false' do - errors = { - authn_context: [t('errors.messages.unauthorized_authn_context')], - } + response = SamlRequestValidator.new.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, + ) - expect(response.to_h).to include( - success: false, - errors: errors, - error_details: hash_including(*errors.keys), - **extra, - ) - end + expect(response.to_h).to include( + success: false, + errors: errors, + error_details: hash_including(*errors.keys), + **extra, + ) end end context 'invalid authn context and invalid sp and authorized nameID format' do - let(:sp) { ServiceProvider.find_by(issuer: 'foo') } - let(:authn_context) { ['IAL1'] } - it 'returns FormResponse with success: false' do + sp = ServiceProvider.find_by(issuer: 'foo') + authn_context = ['IAL1'] + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT + extra = { + authn_context: authn_context, + service_provider: sp&.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'exact', + } errors = { authn_context: [t('errors.messages.unauthorized_authn_context')], service_provider: [t('errors.messages.unauthorized_service_provider')], } + response = SamlRequestValidator.new.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, + ) + expect(response.to_h).to include( success: false, errors: errors, @@ -235,12 +322,26 @@ end context 'valid authn context and sp and unauthorized nameID format' do - let(:name_id_format) { Saml::Idp::Constants::NAME_ID_FORMAT_EMAIL } it 'returns FormResponse with success: false with unauthorized nameid format' do + sp = ServiceProvider.find_by(issuer: 'http://localhost:3000') + authn_context = [Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF] + name_id_format = Saml::Idp::Constants::NAME_ID_FORMAT_EMAIL + extra = { + authn_context: authn_context, + service_provider: sp.issuer, + nameid_format: name_id_format, + authn_context_comparison: 'exact', + } errors = { nameid_format: [t('errors.messages.unauthorized_nameid_format')], } + response = SamlRequestValidator.new.call( + service_provider: sp, + authn_context: authn_context, + nameid_format: name_id_format, + ) + expect(response.to_h).to include( success: false, errors: errors,