diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index 69e00c2d1ea..dce5374cbb6 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -47,7 +47,7 @@ def completions_presenter current_sp: current_sp, decrypted_pii: pii, requested_attributes: decorated_session.requested_attributes.map(&:to_sym), - ial2_requested: sp_session[:ial2] || sp_session[:ialmax], + ial2_requested: ial2_requested?, completion_context: needs_completion_screen_reason, ) end @@ -56,6 +56,14 @@ def ial2? sp_session[:ial2] end + def ial_max? + sp_session[:ialmax] + end + + def ial2_requested? + !!(ial2? || (ial_max? && current_user.identity_verified?)) + end + def return_to_account track_completion_event('account-page') redirect_to account_url diff --git a/app/controllers/users/piv_cac_login_controller.rb b/app/controllers/users/piv_cac_login_controller.rb index ce1b809afdd..80cce49ca71 100644 --- a/app/controllers/users/piv_cac_login_controller.rb +++ b/app/controllers/users/piv_cac_login_controller.rb @@ -89,7 +89,11 @@ def next_step end def ial_context - @ial_context ||= IalContext.new(ial: sp_session_ial, service_provider: current_sp) + @ial_context ||= IalContext.new( + ial: sp_session_ial, + service_provider: current_sp, + user: piv_cac_login_form.user, + ) end def process_invalid_submission diff --git a/app/services/id_token_builder.rb b/app/services/id_token_builder.rb index 879e9bd0925..adb8f950e45 100644 --- a/app/services/id_token_builder.rb +++ b/app/services/id_token_builder.rb @@ -57,7 +57,7 @@ def timestamp_claims def acr ial = identity.ial case ial - when Idp::Constants::IAL_MAX then Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF + when Idp::Constants::IAL_MAX then determine_ial_max_acr when Idp::Constants::IAL1 then Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF when Idp::Constants::IAL2 then Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF else @@ -65,6 +65,14 @@ def acr end end + def determine_ial_max_acr + if identity.user.identity_verified? + Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF + else + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF + end + end + def expires now.to_i + ttl end diff --git a/spec/controllers/sign_up/completions_controller_spec.rb b/spec/controllers/sign_up/completions_controller_spec.rb index a3525044fde..71a76d439d9 100644 --- a/spec/controllers/sign_up/completions_controller_spec.rb +++ b/spec/controllers/sign_up/completions_controller_spec.rb @@ -22,8 +22,8 @@ end context 'IAL1' do - it 'tracks page visit' do - user = create(:user) + let(:user) { create(:user) } + before do stub_sign_in(user) subject.session[:sp] = { issuer: current_sp.issuer, @@ -32,7 +32,9 @@ request_url: 'http://localhost:3000', } get :show + end + it 'tracks page visit' do expect(@analytics).to have_received(:track_event).with( 'User registration: agency handoff visited', ial2: false, @@ -44,6 +46,10 @@ sp_session_requested_attributes: [:email], ) end + + it 'creates a presenter object that is not requesting ial2' do + expect(assigns(:presenter).ial2_requested?).to eq false + end end context 'IAL2' do @@ -61,11 +67,10 @@ request_url: 'http://localhost:3000', } allow(controller).to receive(:user_session).and_return('decrypted_pii' => pii.to_json) + get :show end it 'tracks page visit' do - get :show - expect(@analytics).to have_received(:track_event).with( 'User registration: agency handoff visited', ial2: true, @@ -77,6 +82,56 @@ sp_session_requested_attributes: [:email], ) end + + it 'creates a presenter object that is requesting ial2' do + expect(assigns(:presenter).ial2_requested?).to eq true + end + end + + context 'IALMax' do + let(:user) do + create(:user, profiles: [create(:profile, :verified, :active)]) + end + let(:pii) { { ssn: '123456789' } } + + before do + stub_sign_in(user) + subject.session[:sp] = { + issuer: current_sp.issuer, + ial2: false, + ialmax: true, + requested_attributes: [:email], + request_url: 'http://localhost:3000', + } + allow(controller).to receive(:user_session).and_return('decrypted_pii' => pii.to_json) + get :show + end + + it 'tracks page visit' do + expect(@analytics).to have_received(:track_event).with( + 'User registration: agency handoff visited', + ial2: false, + ialmax: true, + service_provider_name: subject.decorated_session.sp_name, + page_occurence: '', + needs_completion_screen_reason: :new_sp, + sp_request_requested_attributes: nil, + sp_session_requested_attributes: [:email], + ) + end + + context 'verified user' do + it 'creates a presenter object that is requesting ial2' do + expect(assigns(:presenter).ial2_requested?).to eq true + end + end + + context 'unverified user' do + let(:user) { create(:user) } + it 'creates a presenter object that is requesting ial2' do + expect(assigns(:presenter).ial2_requested?).to eq false + end + end end end diff --git a/spec/controllers/users/piv_cac_login_controller_spec.rb b/spec/controllers/users/piv_cac_login_controller_spec.rb new file mode 100644 index 00000000000..6f47682d06b --- /dev/null +++ b/spec/controllers/users/piv_cac_login_controller_spec.rb @@ -0,0 +1,220 @@ +require 'rails_helper' + +describe Users::PivCacLoginController do + describe 'GET new' do + before do + stub_analytics + allow(@analytics).to receive(:track_event) + end + + context 'without a token' do + before { get :new } + + it 'tracks the piv_cac setup' do + expect(@analytics).to have_received(:track_event).with( + 'User Registration: piv cac setup visited', + ) + end + + it 'redirects to root url' do + expect(response).to render_template(:new) + end + end + + context 'with a token' do + let(:token) { 'TEST:abcdefg' } + + context 'an invalid token' do + before { get :new, params: { token: token } } + it 'tracks the login attempt' do + expect(@analytics).to have_received(:track_event).with( + 'PIV/CAC Login', + { + errors: {}, + key_id: nil, + success: false, + }, + ) + end + + it 'redirects to the error url' do + expect(response).to redirect_to(login_piv_cac_error_url(error: 'token.bad')) + end + end + + context 'with a valid token' do + let(:service_provider) { create(:service_provider) } + let(:sp_session) { { ial: 1, issuer: service_provider.issuer } } + let(:nonce) { SecureRandom.base64(20) } + let(:data) do + { + nonce: nonce, + uuid: '1234', + subject: 'subject', + issuer: 'issuer', + }.with_indifferent_access + end + + before do + subject.piv_session[:piv_cac_nonce] = nonce + subject.session[:sp] = sp_session + + allow(PivCacService).to receive(:decode_token).with(token) { data } + get :new, params: { token: token } + end + + context 'without a valid user' do + before do + # valid_token? is being called twice, once to determine if it's a valid submission + # and once to set the session variable in process_invalid_submission + # good opportunity for a refactor + expect(PivCacService).to have_received(:decode_token).with(token) { data }.twice + end + + it 'tracks the login attempt' do + expect(@analytics).to have_received(:track_event).with( + 'PIV/CAC Login', + { + errors: { + type: 'user.not_found', + }, + key_id: nil, + success: false, + }, + ) + end + + it 'sets the session variable' do + expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to be true + end + + it 'redirects to the error url' do + expect(response).to redirect_to(login_piv_cac_error_url(error: 'user.not_found')) + end + end + + context 'with a valid user' do + let(:user) { build(:user) } + let(:piv_cac_config) { create(:piv_cac_configuration, user: user) } + let(:data) do + { + nonce: nonce, + uuid: piv_cac_config.x509_dn_uuid, + subject: 'subject', + issuer: 'issuer', + }.with_indifferent_access + end + + before do + expect(PivCacService).to have_received(:decode_token).with(token) { data } + sign_in user + end + + it 'tracks the login attempt' do + expect(@analytics).to have_received(:track_event).with( + 'PIV/CAC Login', + { + errors: {}, + key_id: nil, + success: true, + }, + ) + end + + it 'sets the session correctly' do + expect(controller.user_session[TwoFactorAuthenticatable::NEED_AUTHENTICATION]). + to eq false + + expect(controller.user_session[:authn_at]).to_not be nil + expect(controller.user_session[:authn_at].class).to eq ActiveSupport::TimeWithZone + end + + it 'tracks the user_marked_authed event' do + expect(@analytics).to have_received(:track_event).with( + 'User marked authenticated', + { authentication_type: :piv_cac }, + ) + end + + it 'saves the piv_cac session information' do + session_info = { + subject: data[:subject], + issuer: data[:issuer], + presented: true, + } + expect(controller.user_session[:decrypted_x509]).to eq session_info.to_json + end + + describe 'it handles the otp_context' do + it 'sets the otp user_session' do + expect(controller.user_session[:auth_method]). + to eq TwoFactorAuthenticatable::AuthMethod::PIV_CAC + + expect(controller.user_session[:unconfirmed_phone]).to be nil + expect(controller.user_session[:context]).to eq 'authentication' + end + + it 'tracks the user_marked_authed event' do + expect(@analytics).to have_received(:track_event).with( + 'User marked authenticated', + { authentication_type: :valid_2fa }, + ) + end + + context 'ial1 user' do + it 'redirects to the after_sign_in_path_for' do + expect(response).to redirect_to(account_url) + end + + context 'ial_max service level' do + let(:sp_session) do + { ial: Idp::Constants::IAL_MAX, issuer: service_provider.issuer } + end + + it 'redirects to the after_sign_in_path_for' do + expect(response).to redirect_to(account_url) + end + end + end + + context 'ial2 user' do + let(:user) { create(:user, profiles: [create(:profile, :verified, :active)]) } + + context 'ial1 service level' do + it 'redirects to the after_sign_in_path_for' do + expect(response).to redirect_to(account_url) + end + end + + context 'ial2 service_level' do + let(:sp_session) { { ial: Idp::Constants::IAL2, issuer: service_provider.issuer } } + + it 'redirects to the capture_password_url' do + expect(response).to redirect_to(capture_password_url) + end + end + + context 'ial_max service_level' do + let(:sp_session) do + { ial: Idp::Constants::IAL_MAX, issuer: service_provider.issuer } + end + + it 'redirects to the capture_password_url' do + expect(response).to redirect_to(capture_password_url) + end + end + end + end + end + end + end + end + + describe 'GET error' do + before { get :error, params: { error: 'token.bad' } } + + it 'sends the error to the error presenter' do + expect(assigns(:presenter).error).to eq 'token.bad' + end + end +end diff --git a/spec/services/id_token_builder_spec.rb b/spec/services/id_token_builder_spec.rb index e67521855c7..6fcb810d10d 100644 --- a/spec/services/id_token_builder_spec.rb +++ b/spec/services/id_token_builder_spec.rb @@ -4,7 +4,7 @@ include Rails.application.routes.url_helpers let(:code) { SecureRandom.hex } - + let(:user) { create(:user) } let(:identity) do build( :service_provider_identity, @@ -15,7 +15,7 @@ # this is a known value from an example developer guide # https://www.pingidentity.com/content/developer/en/resources/openid-connect-developers-guide.html access_token: 'dNZX1hEZ9wBCzNL40Upu646bdzQA', - user: create(:user), + user: user, ) end @@ -61,8 +61,37 @@ expect(decoded_payload[:nonce]).to eq(identity.nonce) end - it 'sets the acr to the request acr' do - expect(decoded_payload[:acr]).to eq(Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF) + context 'it sets the acr' do + context 'ial2 request' do + it 'sets the acr to the ial2 constant' do + expect(decoded_payload[:acr]).to eq(Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF) + end + end + + context 'ial1 request' do + before { identity.ial = 1 } + it 'sets the acr to the ial1 constant' do + expect(decoded_payload[:acr]).to eq(Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF) + end + end + + context 'ialmax request' do + before { identity.ial = 0 } + + context 'non-verified user' do + it 'sets the acr to the ial1 constant' do + expect(decoded_payload[:acr]).to eq(Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF) + end + end + + context 'verified user' do + let(:user) { create(:user, profiles: [create(:profile, :verified, :active)]) } + + it 'sets the acr to the ial2 constant' do + expect(decoded_payload[:acr]).to eq(Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF) + end + end + end end it 'sets the jti to something meaningful' do