diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index 454710e8bac..e20ead20cfd 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -49,10 +49,9 @@ def default_authn_context def link_identity_from_session_data IdentityLinker.new( - current_user, - current_issuer + current_user, current_issuer ).link_identity( - ial: loa3_requested? ? 3 : 1 + ial: ial_level ) end @@ -60,6 +59,10 @@ def identity_needs_verification? loa3_requested? && current_user.decorate.identity_not_verified? end + def ial_level + loa3_requested? ? 3 : 1 + end + def loa3_requested? requested_authn_context == Saml::Idp::Constants::LOA3_AUTHN_CONTEXT_CLASSREF end diff --git a/app/controllers/concerns/verify_sp_attributes_concern.rb b/app/controllers/concerns/verify_sp_attributes_concern.rb new file mode 100644 index 00000000000..7832b819d98 --- /dev/null +++ b/app/controllers/concerns/verify_sp_attributes_concern.rb @@ -0,0 +1,51 @@ +module VerifySPAttributesConcern + def needs_sp_attribute_verification? + if sp_session_identity.nil? || !requested_attributes_verified? + set_verify_shared_attributes_session + true + else + clear_verify_attributes_sessions + false + end + end + + def update_verified_attributes + IdentityLinker.new( + current_user, + sp_session[:issuer] + ).link_identity( + ial: sp_session_ial, + verified_attributes: sp_session[:requested_attributes] + ) + end + + def set_verify_shared_attributes_session + user_session[:verify_shared_attributes] = true + end + + def new_service_provider_attributes + user_session[:verify_shared_attributes] if + user_session.class == ActiveSupport::HashWithIndifferentAccess + end + + def clear_verify_attributes_sessions + user_session[:verify_shared_attributes] = false + end + + private + + def sp_session_identity + @sp_session_identity = + current_user&.identities&.find_by(service_provider: sp_session[:issuer]) + end + + def requested_attributes_verified? + @sp_session_identity && ( + sp_session[:requested_attributes] - @sp_session_identity.verified_attributes.to_a + ).empty? + end + + def sp_session_ial + sp_session[:loa3] ? 3 : 1 + end +end diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 092d829fdd8..9351664c872 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -2,6 +2,7 @@ module OpenidConnect class AuthorizationController < ApplicationController include FullyAuthenticatable include VerifyProfileConcern + include VerifySPAttributesConcern before_action :build_authorize_form_from_params, only: [:index] before_action :validate_authorize_form, only: [:index] @@ -12,18 +13,22 @@ class AuthorizationController < ApplicationController def index return confirm_two_factor_authenticated(request_id) unless user_fully_authenticated? + @authorize_form.link_identity_to_service_provider(current_user, session.id) return redirect_to_account_or_verify_profile_url if profile_or_identity_needs_verification? + return redirect_to(sign_up_completed_url) if needs_sp_attribute_verification? + handle_successful_handoff + end - @authorize_form.link_identity_to_service_provider(current_user, session.id) + private + + def handle_successful_handoff redirect_to @authorize_form.success_redirect_uri delete_branded_experience end - private - def redirect_to_account_or_verify_profile_url - return redirect_to account_or_verify_profile_url if profile_needs_verification? - redirect_to verify_url if identity_needs_verification? + return redirect_to(account_or_verify_profile_url) if profile_needs_verification? + redirect_to(verify_url) if identity_needs_verification? end def profile_or_identity_needs_verification? diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index 342ab97b900..473a75a3f9f 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -8,18 +8,17 @@ class SamlIdpController < ApplicationController include SamlIdpLogoutConcern include FullyAuthenticatable include VerifyProfileConcern + include VerifySPAttributesConcern skip_before_action :verify_authenticity_token def auth return confirm_two_factor_authenticated(request_id) unless user_fully_authenticated? - process_fully_authenticated_user do |needs_idv, needs_profile_finish| - return redirect_to(verify_url) if needs_idv && !needs_profile_finish - return redirect_to(account_or_verify_profile_url) if needs_profile_finish - end - delete_branded_experience - - render_template_for(saml_response, saml_request.response_url, 'SAMLResponse') + link_identity_from_session_data + capture_analytics + return redirect_to_account_or_verify_profile_url if profile_or_identity_needs_verification? + return redirect_to(sign_up_completed_url) if needs_sp_attribute_verification? + handle_successful_handoff end def metadata @@ -39,15 +38,26 @@ def logout private - def process_fully_authenticated_user - link_identity_from_session_data + def redirect_to_account_or_verify_profile_url + return redirect_to(account_or_verify_profile_url) if profile_needs_verification? + redirect_to(verify_url) if identity_needs_verification? + end - needs_idv = identity_needs_verification? - needs_profile_finish = profile_needs_verification? - analytics_payload = @result.to_h.merge(idv: needs_idv, finish_profile: needs_profile_finish) + def profile_or_identity_needs_verification? + profile_needs_verification? || identity_needs_verification? + end + + def capture_analytics + analytics_payload = @result.to_h.merge( + idv: identity_needs_verification?, + finish_profile: profile_needs_verification? + ) analytics.track_event(Analytics::SAML_AUTH, analytics_payload) + end - yield needs_idv, needs_profile_finish + def handle_successful_handoff + delete_branded_experience + render_template_for(saml_response, saml_request.response_url, 'SAMLResponse') end def render_template_for(message, action_url, type) diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index 28cde0368cb..d190ce022ca 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -1,6 +1,7 @@ module SignUp class CompletionsController < ApplicationController include SecureHeadersConcern + include VerifySPAttributesConcern before_action :confirm_two_factor_authenticated before_action :verify_confirmed, if: :loa3? @@ -21,7 +22,8 @@ def update track_agency_handoff( Analytics::USER_REGISTRATION_AGENCY_HANDOFF_COMPLETE ) - + update_verified_attributes + clear_verify_attributes_sessions if decider.go_back_to_mobile_app? sign_user_out_and_instruct_to_go_back_to_mobile_app else @@ -40,7 +42,8 @@ def view_model SignUpCompletionsShow.new( loa3_requested: loa3?, decorated_session: decorated_session, - current_user: current_user + current_user: current_user, + handoff: new_service_provider_attributes ) end diff --git a/app/decorators/service_provider_session_decorator.rb b/app/decorators/service_provider_session_decorator.rb index f822a7c0e36..7779e0f64dd 100644 --- a/app/decorators/service_provider_session_decorator.rb +++ b/app/decorators/service_provider_session_decorator.rb @@ -65,7 +65,7 @@ def idv_hardfail4_partial end def requested_attributes - sp_session[:requested_attributes] + sp_session[:requested_attributes].sort end def sp_name diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index 0c7d98723cd..4ee64191fd9 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -56,7 +56,7 @@ def submit end def loa3_requested? - ial == 3 + loa == 3 end def sp_redirect_uri @@ -87,6 +87,11 @@ def success_redirect_uri attr_reader :identity, :success, :openid_connect_redirector, :already_linked + def requested_attributes + @requested_attributes ||= + OpenidConnectAuthorizeDecorator.new(scopes: scope).requested_attributes + end + def parse_to_values(param_value, possible_values) return [] if param_value.blank? param_value.split(' ').compact & possible_values @@ -111,7 +116,7 @@ def validate_scope errors.add(:scope, t('openid_connect.authorization.errors.no_valid_scope')) end - def ial + def loa case acr_values.sort.max when Saml::Idp::Constants::LOA1_AUTHN_CONTEXT_CLASSREF 1 @@ -120,6 +125,10 @@ def ial end end + def ial + loa == 3 ? 3 : 1 + end + def extra_analytics_attributes { client_id: client_id, diff --git a/app/services/identity_linker.rb b/app/services/identity_linker.rb index 3f3e6ddac87..d54d0d8422d 100644 --- a/app/services/identity_linker.rb +++ b/app/services/identity_linker.rb @@ -43,7 +43,8 @@ def optional_attributes( ial: nil, nonce: nil, rails_session_id: nil, - scope: nil + scope: nil, + verified_attributes: nil ) { code_challenge: code_challenge, @@ -51,6 +52,12 @@ def optional_attributes( nonce: nonce, rails_session_id: rails_session_id, scope: scope, + verified_attributes: merge_attributes(verified_attributes), } end + + def merge_attributes(verified_attributes) + verified_attributes = verified_attributes.to_a.map(&:to_s) + (identity.verified_attributes.to_a + verified_attributes).uniq.sort + end end diff --git a/app/view_models/sign_up_completions_show.rb b/app/view_models/sign_up_completions_show.rb index 3ed98e22144..48f7989a4fc 100644 --- a/app/view_models/sign_up_completions_show.rb +++ b/app/view_models/sign_up_completions_show.rb @@ -1,10 +1,11 @@ class SignUpCompletionsShow include ActionView::Helpers::TagHelper - def initialize(loa3_requested:, decorated_session:, current_user:) + def initialize(loa3_requested:, decorated_session:, current_user:, handoff:) @loa3_requested = loa3_requested @decorated_session = decorated_session @current_user = current_user + @handoff = handoff end attr_reader :loa3_requested, :decorated_session @@ -22,15 +23,16 @@ def initialize(loa3_requested:, decorated_session:, current_user:) # rubocop:disable Rails/OutputSafety def heading + return content_tag(:strong, I18n.t('titles.sign_up.new_sp')) if handoff? if requested_loa == 'loa3' - content_tag(:strong, I18n.t('titles.sign_up.verified', app: APP_NAME)) - else - safe_join([I18n.t( - 'titles.sign_up.completion_html', - accent: content_tag(:strong, I18n.t('titles.sign_up.loa1')), - app: APP_NAME - ).html_safe]) + return content_tag(:strong, I18n.t('titles.sign_up.verified', app: APP_NAME)) end + + safe_join([I18n.t( + 'titles.sign_up.completion_html', + accent: content_tag(:strong, I18n.t('titles.sign_up.loa1')), + app: APP_NAME + ).html_safe]) end # rubocop:enable Rails/OutputSafety @@ -92,6 +94,10 @@ def user_has_identities? private + def handoff? + @handoff + end + def requested_attributes decorated_session.requested_attributes.map(&:to_sym) end diff --git a/config/locales/titles/en.yml b/config/locales/titles/en.yml index 33f3f7cd4d5..59a4f45281a 100644 --- a/config/locales/titles/en.yml +++ b/config/locales/titles/en.yml @@ -24,6 +24,7 @@ en: completion_html: You have %{accent} with %{app} loa1: created your account verified: You have verified your identity with %{app} + new_sp: You are now logging in for the first time totp_setup: new: Set up two-factor authentication two_factor_setup: Two-factor authentication setup diff --git a/config/locales/titles/es.yml b/config/locales/titles/es.yml index 2a1e29781da..db71abf9031 100644 --- a/config/locales/titles/es.yml +++ b/config/locales/titles/es.yml @@ -24,6 +24,7 @@ es: completion_html: Tiene %{accent} con %{app} loa1: creó su cuenta verified: Tiene verificó su identidad con %{app} + new_sp: NOT TRANSLATED YET totp_setup: new: Configure la autenticación de dos factores two_factor_setup: Configuración de autenticación de dos factores diff --git a/config/locales/titles/fr.yml b/config/locales/titles/fr.yml index 62f1f7e0579..27a5b254b6c 100644 --- a/config/locales/titles/fr.yml +++ b/config/locales/titles/fr.yml @@ -24,6 +24,7 @@ fr: completion_html: Vous avez %{accent} avec %{app} loa1: créé votre compte verified: Vous avez verifié votre identité avec %{app} + new_sp: NOT TRANSLATED YET totp_setup: new: Configurer l'authentification à deux facteurs two_factor_setup: Configuration de l'authentification à deux facteurs diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index d7713d3a58f..f139190781d 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -26,6 +26,8 @@ context 'with valid params' do it 'redirects back to the client app with a code' do + IdentityLinker.new(user, client_id).link_identity(ial: 1) + user.identities.last.update!(verified_attributes: ["given_name", "family_name", "birthdate"]) action expect(response).to redirect_to(/^#{params[:redirect_uri]}/) @@ -54,6 +56,8 @@ let(:user) { create(:profile, :active, :verified).user } it 'redirects to the redirect_uri immediately' do + IdentityLinker.new(user, client_id).link_identity(ial: 3) + user.identities.last.update!(verified_attributes: ["given_name", "family_name", "birthdate"]) action expect(response).to redirect_to(/^#{params[:redirect_uri]}/) @@ -68,15 +72,35 @@ end end + context 'user has not approved this application' do + it 'redirects verify shared attributes page' do + action + + expect(response).to redirect_to(sign_up_completed_url) + end + + it 'links identity to the user' do + action + sp = user.identities.last.service_provider + expect(sp).to eq(params[:client_id]) + end + end + context 'user has already approved this application' do before do IdentityLinker.new(user, client_id).link_identity + user.identities.last.update!(verified_attributes: ["given_name", "family_name", "birthdate"]) end - it 'redirects to the redirect_uri immediately' do + it 'redirects back to the client app with a code' do action expect(response).to redirect_to(/^#{params[:redirect_uri]}/) + + redirect_params = URIService.params(response.location) + + expect(redirect_params[:code]).to be_present + expect(redirect_params[:state]).to eq(params[:state]) end end end diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index ac9f16f87b0..c6f1353cec5 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -159,6 +159,8 @@ before do stub_sign_in(user) + IdentityLinker.new(user, loa3_saml_settings.issuer).link_identity(ial: 3) + user.identities.last.update!(verified_attributes: ["given_name", "family_name", "social_security_number", "address"]) allow(subject).to receive(:attribute_asserter) { asserter } end @@ -242,7 +244,10 @@ allow(@analytics).to receive(:track_event) user = create(:user, :signed_up) - generate_saml_response(user, missing_authn_context_saml_settings) + auth_settings = missing_authn_context_saml_settings + IdentityLinker.new(user, auth_settings.issuer).link_identity + user.identities.last.update!(verified_attributes: ['email']) + generate_saml_response(user, auth_settings) expect(response.status).to eq(200) @@ -333,22 +338,51 @@ end context 'after successful assertion of loa1' do + let(:user_identity){ + @user.identities.find_by(service_provider: saml_settings.issuer) + } before do sign_in(@user) saml_get_auth(saml_settings) - @user_identity = @user.identities.find_by(service_provider: saml_settings.issuer) end it 'does not delete SP metadata from session' do expect(session.key?(:sp)).to eq(true) end - it 'links the user to the service provider' do - expect(@user_identity).to_not be_nil + it 'does links the user to the service provider' do + expect(user_identity).to_not be_nil + end + + it 'sets verified attributes on the identity to nothing' do + expect(user_identity.verified_attributes).to eq([]) + end + + it 'sets user identity loa value to 1 after verifying attributes' do + saml_get_auth(saml_settings) + expect(user_identity.ial).to eq(1) end - it 'sets user identity loa value to 1' do - expect(@user_identity.ial).to eq(1) + it 'redirects to verify attributes' do + expect(response).to redirect_to sign_up_completed_url + expect(subject.user_session.key?(:verify_shared_attributes)).to eq(true) + end + + it 'does not redirect after verifying attributes' do + IdentityLinker.new(@user, saml_settings.issuer).link_identity( + verified_attributes: ["email"] + ) + saml_get_auth(saml_settings) + + expect(response).to_not redirect_to sign_up_completed_url + end + + it 'redirects if verified attributes dont match requested attributes' do + saml_get_auth(saml_settings) + + user_identity.update!(verified_attributes: nil) + saml_get_auth(saml_settings) + expect(response).to redirect_to sign_up_completed_url end end end @@ -449,20 +483,15 @@ context 'after signing in' do before do user = create(:user, :signed_up) - generate_saml_response(user) + generate_saml_response(user, link: false) end - it 'calls IdentityLinker' do + it 'does not call IdentityLinker' do user = create(:user, :signed_up) - + linker = instance_double(IdentityLinker) - expect(IdentityLinker).to receive(:new). - with(controller.current_user, saml_settings.issuer).and_return(linker) - - expect(linker).to receive(:link_identity) - - generate_saml_response(user) + expect(IdentityLinker).to_not receive(:new) end end diff --git a/spec/controllers/sign_up/completions_controller_spec.rb b/spec/controllers/sign_up/completions_controller_spec.rb index 94662cb6f41..dc37ed0d06c 100644 --- a/spec/controllers/sign_up/completions_controller_spec.rb +++ b/spec/controllers/sign_up/completions_controller_spec.rb @@ -10,7 +10,8 @@ context 'LOA1' do it 'tracks page visit' do - stub_sign_in + user = create(:user) + stub_sign_in(user) subject.session[:sp] = { issuer: 'awesome sp', loa3: false } get :show @@ -69,7 +70,8 @@ end it 'renders show if the user has an sp in the active session' do - stub_sign_in + user = create(:user) + stub_sign_in(user) subject.session[:sp] = { issuer: 'awesome sp', loa3: false } get :show @@ -91,6 +93,9 @@ before do stub_analytics allow(@analytics).to receive(:track_event) + @linker = instance_double(IdentityLinker) + allow(@linker).to receive(:link_identity).and_return(true) + allow(IdentityLinker).to receive(:new).and_return(@linker) end context 'LOA1' do @@ -108,6 +113,17 @@ loa3: false, service_provider_name: subject.decorated_session.sp_name ) end + + it 'updates verified attributes' do + stub_sign_in + subject.session[:sp] = { + loa3: false, + request_url: 'http://example.com', + requested_attributes: ["email"] + } + expect(@linker).to receive(:link_identity).with(ial: 1 , verified_attributes: ["email"]) + patch :update + end end context 'LOA3' do @@ -126,6 +142,18 @@ loa3: true, service_provider_name: subject.decorated_session.sp_name ) end + + it 'updates verified attributes' do + user = create(:user, profiles: [create(:profile, :verified, :active)]) + stub_sign_in(user) + subject.session[:sp] = { + loa3: true, + request_url: 'http://example.com', + requested_attributes: ["email", "first_name"] + } + expect(@linker).to receive(:link_identity).with(ial: 3 , verified_attributes: ["email", "first_name"]) + patch :update + end end end end diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index 8b8cbffcc34..d01c8af0829 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -58,6 +58,7 @@ user = user_with_2fa IdentityLinker.new(user, client_id).link_identity + user.identities.last.update!(verified_attributes: ["email"]) visit openid_connect_authorize_path( client_id: client_id, @@ -87,6 +88,7 @@ user = user_with_2fa IdentityLinker.new(user, client_id).link_identity + user.identities.last.update!(verified_attributes: ["email"]) visit openid_connect_authorize_path( client_id: client_id, @@ -125,6 +127,10 @@ nonce = SecureRandom.hex code_verifier = SecureRandom.hex code_challenge = Digest::SHA256.base64digest(code_verifier) + user = user_with_2fa + + link_identity(user, client_id) + user.identities.last.update!(verified_attributes: ['email']) visit openid_connect_authorize_path( client_id: client_id, @@ -139,7 +145,7 @@ code_challenge_method: 'S256' ) - _user = sign_in_live_with_2fa + _user = sign_in_live_with_2fa(user) expect(page.html).to_not include(code_challenge) redirect_uri = URI(current_url) @@ -223,6 +229,43 @@ end end + context 'logging into an SP for the first time' do + it 'displays shared attributes page once' do + client_id = 'urn:gov:gsa:openidconnect:sp:server' + + user = user_with_2fa + + oidc_path = openid_connect_authorize_path( + client_id: client_id, + response_type: 'code', + acr_values: Saml::Idp::Constants::LOA1_AUTHN_CONTEXT_CLASSREF, + scope: 'openid email', + redirect_uri: 'http://localhost:7654/auth/result', + state: SecureRandom.hex, + nonce: SecureRandom.hex, + prompt: 'select_account' + ) + visit oidc_path + + sign_in_live_with_2fa(user) + + + expect(current_url).to eq(sign_up_completed_url) + expect(page).to have_content(t('titles.sign_up.new_sp')) + + click_continue + expect(current_url).to start_with('http://localhost:7654/auth/result') + visit sign_out_url + visit oidc_path + sign_in_live_with_2fa(user) + + redirect_uri = URI(current_url) + + expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result') + + end + end + context 'going back to the SP' do it 'links back to the SP from the sign in page' do state = SecureRandom.hex @@ -377,6 +420,10 @@ def sign_in_get_id_token nonce = SecureRandom.hex code_verifier = SecureRandom.hex code_challenge = Digest::SHA256.base64digest(code_verifier) + user = user_with_2fa + + link_identity(user, client_id) + user.identities.last.update!(verified_attributes: ["email"]) visit openid_connect_authorize_path( client_id: client_id, @@ -391,7 +438,7 @@ def sign_in_get_id_token code_challenge_method: 'S256' ) - _user = sign_in_live_with_2fa + _user = sign_in_live_with_2fa(user) redirect_uri = URI(current_url) redirect_params = Rack::Utils.parse_query(redirect_uri.query).with_indifferent_access @@ -451,7 +498,7 @@ def oidc_end_client_secret_jwt(prompt: nil, user: nil, redirs_to: nil) pii: { first_name: 'John', ssn: '111223333' }).user sign_in_live_with_2fa(user) - + click_continue redirect_uri = URI(current_url) redirect_params = Rack::Utils.parse_query(redirect_uri.query).with_indifferent_access diff --git a/spec/features/saml/idp_initiated_slo_spec.rb b/spec/features/saml/idp_initiated_slo_spec.rb index e7cdb2ba238..75a0d3bf28d 100644 --- a/spec/features/saml/idp_initiated_slo_spec.rb +++ b/spec/features/saml/idp_initiated_slo_spec.rb @@ -14,6 +14,7 @@ before do sign_in_and_2fa_user(user) visit sp1_authnrequest + click_continue @asserted_session_index = response_xmldoc.assertion_statement_node['SessionIndex'] visit destroy_user_session_url @@ -51,12 +52,14 @@ before do sign_in_and_2fa_user(logout_user) visit sp1_authnrequest + click_continue @sp1_asserted_session_index = response_xmldoc.assertion_statement_node['SessionIndex'] click_button t('forms.buttons.submit.default') visit sp2_authnrequest + click_continue @sp2_asserted_session_index = response_xmldoc.assertion_statement_node['SessionIndex'] click_button t('forms.buttons.submit.default') end diff --git a/spec/features/saml/loa1_sso_spec.rb b/spec/features/saml/loa1_sso_spec.rb index 6b660f8514b..918f08eb63c 100644 --- a/spec/features/saml/loa1_sso_spec.rb +++ b/spec/features/saml/loa1_sso_spec.rb @@ -44,6 +44,7 @@ click_link t('links.sign_in') fill_in_credentials_and_submit(user.email, user.password) click_submit_default + click_continue expect(current_url).to eq saml_authn_request @@ -122,11 +123,46 @@ visit new_user_session_url(request_id: sp_request_id) fill_in_credentials_and_submit(user.email, user.password) click_submit_default + click_continue expect(current_url).to eq saml_authn_request end end + context 'fully signed up user authenticates new sp' do + let(:user){ create(:user, :signed_up) } + let(:saml_authn_request){ auth_request.create(saml_settings) } + + before do + allow(FeatureManagement).to receive(:prefill_otp_codes?).and_return(true) + sign_in_user(user) + click_submit_default + visit saml_authn_request + end + + it 'redirects user to verify attributes page' do + expect(current_url).to eq(sign_up_completed_url) + expect(page).to have_content(t('titles.sign_up.new_sp')) + end + + it 'returns to sp after clicking continue' do + click_continue + expect(current_url).to eq(saml_authn_request) + end + + it 'it immediately returns to the SP after signing in again' do + click_continue + + visit sign_out_url + + sign_in_user(user) + click_submit_default + + visit saml_authn_request + expect(current_url).to eq(saml_authn_request) + end + end + context 'fully signed up user is signed in with email and password only' do it 'prompts to enter OTP' do user = create(:user, :signed_up) diff --git a/spec/features/saml/saml_spec.rb b/spec/features/saml/saml_spec.rb index 505085db8ba..644272d76fb 100644 --- a/spec/features/saml/saml_spec.rb +++ b/spec/features/saml/saml_spec.rb @@ -44,6 +44,7 @@ before do sign_in_and_2fa_user(user) visit sp1_authnrequest + click_continue end let(:xmldoc) { SamlResponseDoc.new('feature', 'response_assertion') } @@ -57,6 +58,7 @@ before do sign_in_and_2fa_user(user) visit authnrequest_get + click_continue end let(:xmldoc) { SamlResponseDoc.new('feature', 'response_assertion') } diff --git a/spec/features/saml/sp_initiated_slo_spec.rb b/spec/features/saml/sp_initiated_slo_spec.rb index 58949503775..c39eaf7ca5d 100644 --- a/spec/features/saml/sp_initiated_slo_spec.rb +++ b/spec/features/saml/sp_initiated_slo_spec.rb @@ -10,7 +10,7 @@ before do sign_in_and_2fa_user(user) visit sp1_authnrequest - + click_continue sp1 = ServiceProvider.from_issuer(sp1_saml_settings.issuer) settings = sp1_saml_settings settings.security[:embed_sign] = false @@ -34,6 +34,7 @@ before do sign_in_and_2fa_user(user) visit sp1_authnrequest + click_continue sp1 = ServiceProvider.from_issuer(sp1_saml_settings.issuer) settings = sp1_saml_settings @@ -91,11 +92,13 @@ before do sign_in_and_2fa_user(user) visit sp1_authnrequest # sp1 + click_continue @sp1_asserted_session_index = response_xmldoc.assertion_statement_node['SessionIndex'] click_button t('forms.buttons.submit.default') visit sp2_authnrequest # sp2 + click_continue @sp2_asserted_session_index = response_xmldoc.assertion_statement_node['SessionIndex'] click_button t('forms.buttons.submit.default') @@ -143,11 +146,13 @@ before do sign_in_and_2fa_user(user) visit sp1_authnrequest # sp1 + click_continue @sp1_session_index = response_xmldoc.response_session_index_assertion click_button t('forms.buttons.submit.default') visit sp2_authnrequest # sp2 + click_continue @sp2_session_index = response_xmldoc.response_session_index_assertion click_button t('forms.buttons.submit.default') @@ -182,11 +187,13 @@ before do sign_in_and_2fa_user(user) visit sp2_authnrequest # sp2 + click_continue @sp2_session_index = response_xmldoc.response_session_index_assertion click_button t('forms.buttons.submit.default') visit sp1_authnrequest # sp1 + click_continue @sp1_session_index = response_xmldoc.response_session_index_assertion click_button t('forms.buttons.submit.default') @@ -227,10 +234,12 @@ sign_in_and_2fa_user(user) visit sp1_authnrequest # sp1 + click_continue @browser_one_sp1_session_index = response_xmldoc.response_session_index_assertion click_submit_default visit sp2_authnrequest # sp2 + click_continue @browser_one_sp2_session_index = response_xmldoc.response_session_index_assertion click_submit_default end @@ -281,6 +290,7 @@ before do sign_in_and_2fa_user(logout_user) visit sp1_authnrequest + click_continue click_button t('forms.buttons.submit.default') end @@ -303,9 +313,10 @@ before do sign_in_and_2fa_user(user) visit sp1_authnrequest - + click_continue sp1 = ServiceProvider.from_issuer(sp1_saml_settings.issuer) settings = sp1_saml_settings + settings.name_identifier_value = user.decorate.active_identity_for(sp1).uuid Timecop.travel(Devise.timeout_in + 1.second) diff --git a/spec/support/controller_helper.rb b/spec/support/controller_helper.rb index d87866b2249..217d225ad73 100644 --- a/spec/support/controller_helper.rb +++ b/spec/support/controller_helper.rb @@ -51,6 +51,10 @@ def stub_decorated_user_with_pending_profile(user) and_return(has_pending_profile) decorated_user end + + def stub_identity(user, params) + Identity.new(params.merge({user: user})).save + end end RSpec.configure do |config| diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 65b78626bf1..106fed5222c 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -129,7 +129,7 @@ def click_submit_default end def click_continue - click_button t('forms.buttons.continue') + click_button t('forms.buttons.continue') if page.has_button?(t('forms.buttons.continue')) end def enter_correct_otp_code_for_user(user) @@ -379,5 +379,14 @@ def stub_twilio_service allow(TwilioService).to receive(:new).and_return(twilio_service) end + + def link_identity(user, client_id, ial = nil) + IdentityLinker.new( + user, + client_id + ).link_identity( + ial: ial + ) + end end end diff --git a/spec/support/idv_examples/usps_verification.rb b/spec/support/idv_examples/usps_verification.rb index d35619ce079..900bb40c587 100644 --- a/spec/support/idv_examples/usps_verification.rb +++ b/spec/support/idv_examples/usps_verification.rb @@ -51,7 +51,6 @@ sign_in_from_sp(sp) usps_confirmation_code.update(code_sent_at: 11.days.ago) - fill_in t('forms.verify_profile.name'), with: otp click_button t('forms.verify_profile.submit') diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb index 7f08b0d2bce..d6d75d67cce 100644 --- a/spec/support/saml_auth_helper.rb +++ b/spec/support/saml_auth_helper.rb @@ -163,8 +163,9 @@ def saml_test_key end # generates a SAML response and returns a parsed Nokogiri XML document - def generate_saml_response(user, settings = saml_settings) + def generate_saml_response(user, settings = saml_settings, link: true) # user needs to be signed in in order to generate an assertion + link_user_to_identity(user, link, settings) sign_in(user) saml_get_auth(settings) end @@ -176,6 +177,22 @@ def saml_get_auth(settings) private + def link_user_to_identity(user, link, settings) + if link + IdentityLinker.new( + user, + settings.issuer + ).link_identity( + ial: loa3_requested?(settings) ? true : nil, + verified_attributes: ['email'] + ) + end + end + + def loa3_requested?(settings) + settings.authn_context != Saml::Idp::Constants::LOA1_AUTHN_CONTEXT_CLASSREF + end + def saml_request(settings) authn_request(settings).split('SAMLRequest=').last end diff --git a/spec/support/shared_examples/account_creation.rb b/spec/support/shared_examples/account_creation.rb index 8b95723bb89..5b3497c9d20 100644 --- a/spec/support/shared_examples/account_creation.rb +++ b/spec/support/shared_examples/account_creation.rb @@ -37,7 +37,6 @@ end click_on t('forms.buttons.continue') - expect(current_url).to eq @saml_authn_request if sp == :saml if sp == :oidc diff --git a/spec/support/shared_examples/sign_in.rb b/spec/support/shared_examples/sign_in.rb index d673cb83b78..c318e32237a 100644 --- a/spec/support/shared_examples/sign_in.rb +++ b/spec/support/shared_examples/sign_in.rb @@ -14,6 +14,9 @@ end click_submit_default + expect(current_url).to eq(sign_up_completed_url(locale: 'es')) + + click_continue expect(current_url).to eq @saml_authn_request if sp == :saml diff --git a/spec/view_models/sign_up_completions_show_spec.rb b/spec/view_models/sign_up_completions_show_spec.rb index 5ccd31dfbe5..074dfce5891 100644 --- a/spec/view_models/sign_up_completions_show_spec.rb +++ b/spec/view_models/sign_up_completions_show_spec.rb @@ -9,7 +9,8 @@ SignUpCompletionsShow.new( current_user: @user, loa3_requested: false, - decorated_session: decorated_session + decorated_session: decorated_session, + handoff: false ) end diff --git a/spec/views/sign_up/completions/show.html.slim_spec.rb b/spec/views/sign_up/completions/show.html.slim_spec.rb index 33af99471f5..85e8e38f386 100644 --- a/spec/views/sign_up/completions/show.html.slim_spec.rb +++ b/spec/views/sign_up/completions/show.html.slim_spec.rb @@ -6,7 +6,8 @@ @view_model = SignUpCompletionsShow.new( current_user: @user, loa3_requested: false, - decorated_session: SessionDecorator.new + decorated_session: SessionDecorator.new, + handoff: false ) end @@ -28,6 +29,22 @@ expect(rendered).to have_content(content) end + context 'loging into sp for the first time after account creation' do + before do + @view_model = SignUpCompletionsShow.new( + current_user: @user, + loa3_requested: false, + decorated_session: SessionDecorator.new, + handoff: true + ) + create_identities(@user) + end + it 'informs user they are logging into an SP for the first time' do + render + expect(rendered).to have_content(t('titles.sign_up.new_sp')) + end + end + private def create_identities(user, count = 0)