diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 4b899f33bc9..8004002d134 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -28,7 +28,7 @@ class AuthorizationController < ApplicationController def index if @authorize_form.ial2_or_greater? return redirect_to reactivate_account_url if user_needs_to_reactivate_account? - return redirect_to url_for_pending_profile_reason if user_has_pending_profile? + return redirect_to url_for_pending_profile_reason if user_has_usable_pending_profile? return redirect_to idv_url if identity_needs_verification? return redirect_to idv_url if selfie_needed? end @@ -47,6 +47,18 @@ def index private + def pending_profile_policy + @pending_profile_policy ||= PendingProfilePolicy.new( + user: current_user, + resolved_authn_context_result: resolved_authn_context_result, + biometric_comparison_requested: biometric_comparison_requested?, + ) + end + + def user_has_usable_pending_profile? + pending_profile_policy.user_has_usable_pending_profile? + end + def block_biometric_requests_in_production if biometric_comparison_requested? && !FeatureManagement.idv_allow_selfie_check? diff --git a/app/policies/pending_profile_policy.rb b/app/policies/pending_profile_policy.rb new file mode 100644 index 00000000000..fc9d3a0c858 --- /dev/null +++ b/app/policies/pending_profile_policy.rb @@ -0,0 +1,40 @@ +class PendingProfilePolicy + def initialize(user:, resolved_authn_context_result:, biometric_comparison_requested:) + @user = user + @resolved_authn_context_result = resolved_authn_context_result + @biometric_comparison_requested = biometric_comparison_requested + end + + def user_has_usable_pending_profile? + if biometric_comparison_requested? + pending_biometric_profile? + else + pending_legacy_profile? || fraud_review_pending? + end + end + + private + + attr_reader :user, :resolved_authn_context_result, :biometric_comparison_requested + + def active_biometric_profile? + user.active_profile&.idv_level == 'unsupervised_with_selfie' + end + + def pending_biometric_profile? + user.pending_profile&.idv_level == 'unsupervised_with_selfie' + end + + def biometric_comparison_requested? + return false if !FeatureManagement.idv_allow_selfie_check? + resolved_authn_context_result.biometric_comparison? || biometric_comparison_requested + end + + def pending_legacy_profile? + user.pending_profile.present? && user.pending_profile&.idv_level != 'unsupervised_with_selfie' + end + + def fraud_review_pending? + user.fraud_review_pending? + end +end diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index fd70342d6b9..36462739077 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -479,6 +479,84 @@ end end + context 'verified non-biometric profile with pending biometric profile' do + before do + allow(IdentityConfig.store).to receive(:openid_connect_redirect). + and_return('server_side') + IdentityLinker.new(user, service_provider).link_identity(ial: 3) + user.identities.last.update!( + verified_attributes: %w[birthdate family_name given_name verified_at], + ) + allow(controller).to receive(:pii_requested_but_locked?).and_return(false) + end + + context 'sp does not request biometrics' do + let(:selfie_capture_enabled) { true } + let(:user) { create(:profile, :active, :verified).user } + + before do + expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once). + and_return(selfie_capture_enabled) + end + + it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do + create(:profile, :verify_by_mail_pending, :with_pii, idv_level: :unsupervised_with_selfie, user: user) + user.active_profile.idv_level = :legacy_unsupervised + + action + + expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + expect(user.identities.last.verified_attributes).to eq(%w[birthdate family_name given_name verified_at]) + end + + it 'redirects to please call page if user has a fraudualent profile' do + create(:profile, :fraud_review_pending, :with_pii, idv_level: :unsupervised_with_selfie, user: user) + + action + + expect(response).to redirect_to(idv_please_call_url) + end + end + + context 'sp requests biometrics' do + let(:selfie_capture_enabled) { true } + let(:user) { create(:profile, :active, :verified).user } + + before do + expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once). + and_return(selfie_capture_enabled) + end + + context 'with biometric_comparison_required param' do + before do + params[:biometric_comparison_required] = 'true' + end + + it 'redirects to gpo enter code page' do + create(:profile, :verify_by_mail_pending, idv_level: :unsupervised_with_selfie, user: user) + + action + + expect(controller).to redirect_to(idv_verify_by_mail_enter_code_url) + end + end + + context 'with vectors of trust' do + before do + params[:vtr] = ['C1.C2.P1.Pb'].to_json + end + + it 'redirects to gpo enter code page' do + create(:profile, :verify_by_mail_pending, idv_level: :unsupervised_with_selfie, user: user) + + action + + expect(controller).to redirect_to(idv_verify_by_mail_enter_code_url) + end + end + end + end + context 'account is not already verified' do it 'redirects to have the user verify their account' do action diff --git a/spec/policies/pending_profile_policy_spec.rb b/spec/policies/pending_profile_policy_spec.rb new file mode 100644 index 00000000000..bd579095011 --- /dev/null +++ b/spec/policies/pending_profile_policy_spec.rb @@ -0,0 +1,80 @@ +require 'rails_helper' + +RSpec.describe PendingProfilePolicy do + let(:user) { create(:user) } + let(:resolved_authn_context_result) do + AuthnContextResolver.new( + service_provider: nil, + vtr: vtr, + acr_values: acr_values, + ).resolve + end + let(:biometric_comparison_requested) { nil } + let(:vtr) { nil } + let(:acr_values) { nil } + + subject(:policy) do + described_class.new( + user: user, + resolved_authn_context_result: resolved_authn_context_result, + biometric_comparison_requested: biometric_comparison_requested, + ) + end + + describe '#user_has_usable_pending_profile?' do + context 'has an active non-biometric profile and biometric comparison is requested' do + let(:idv_level) { :unsupervised_with_selfie } + before do + create(:profile, :active, :verified, idv_level: :legacy_unsupervised, user: user) + create(:profile, :verify_by_mail_pending, idv_level: idv_level, user: user) + allow(FeatureManagement).to receive(:idv_allow_selfie_check?).and_return(true) + end + + context 'with resolved authn context result' do + let(:vtr) { ['C2.Pb'] } + + it 'has a usable pending profile' do + expect(policy.user_has_usable_pending_profile?).to eq(true) + end + end + + context 'with biometric_comparison_requested param set to true' do + let(:biometric_comparison_requested) { true } + let(:acr_values) { Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF } + + it 'has a usable pending profile' do + expect(policy.user_has_usable_pending_profile?).to eq(true) + end + end + end + + context 'no biometric comparison is requested' do + let(:idv_level) { :legacy_unsupervised } + let(:vtr) { ['C2'] } + context 'user has pending profile' do + before do + create(:profile, :verify_by_mail_pending, idv_level: idv_level, user: user) + end + + it { expect(policy.user_has_usable_pending_profile?).to eq(true) } + end + + context 'user has an active profile' do + before do + create(:profile, :active, :verified, idv_level: idv_level, user: user) + end + + it { expect(policy.user_has_usable_pending_profile?).to eq(false) } + end + + context 'user has active legacy profile with a pending fraud biometric profile' do + before do + create(:profile, :active, :verified, idv_level: idv_level, user: user) + create(:profile, :fraud_review_pending, idv_level: :unsupervised_with_selfie, user: user) + end + + it { expect(policy.user_has_usable_pending_profile?).to eq(true) } + end + end + end +end