diff --git a/.circleci/config.yml b/.circleci/config.yml index 14536085ace..3bbfde3c3f4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: parallelism: 4 docker: # Specify the Ruby version you desire here - - image: circleci/ruby:2.5.1-node-browsers + - image: circleci/ruby:2.5.3-node-browsers environment: RAILS_ENV: test CC_TEST_REPORTER_ID: faecd27e9aed532634b3f4d3e251542d7de9457cfca96a94208a63270ef9b42e @@ -46,7 +46,7 @@ jobs: - v1-identity-idp-yarn- - run: name: Install Yarn - command: yarn install --cache-folder ~/.cache/yarn + command: yarn install --ignore-engines --cache-folder ~/.cache/yarn - save-cache: key: v1-identity-idp-yarn-{{ checksum "yarn.lock" }} paths: diff --git a/.gitignore b/.gitignore index d2424706337..c088cfd8a79 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ Vagrantfile !/cert/*.crt.example /config/application.yml /config/aws.yml +/geo_data/* /keys/*.key.enc !/keys/*.key.enc.example /keys/equifax_rsa diff --git a/.reek.yml b/.reek.yml index faf5dba8de9..ebfc222574d 100644 --- a/.reek.yml +++ b/.reek.yml @@ -128,10 +128,13 @@ detectors: - TwoFactorLoginOptionsPresenter UncommunicativeMethodName: exclude: + - Deploy::Activate#download_application_yml_from_s3 + - Deploy::Activate#download_geocoding_database_from_s3 - PhoneConfirmationFlow - render_401 - SessionDecorator#registration_bullet_1 - ServiceProviderSessionDecorator#registration_bullet_1 + UncommunicativeModuleName: exclude: - X509 diff --git a/.rubocop.yml b/.rubocop.yml index e962e136b39..4ff7af9ca0f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -67,6 +67,7 @@ Metrics/ClassLength: - app/services/analytics.rb - app/services/idv/session.rb - app/presenters/two_factor_auth_code/phone_delivery_presenter.rb + - app/view_models/account_show.rb - lib/cloudhsm/cloudhsm_key_generator.rb Metrics/LineLength: diff --git a/Gemfile b/Gemfile index a5bace60a8e..324b2aa639d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' git_source(:github) { |repo_name| "https://github.com/#{repo_name}.git" } -ruby '~> 2.5.1' +ruby '~> 2.5.3' gem 'rails', '~> 5.1.3' @@ -27,6 +27,7 @@ gem 'identity-hostdata', github: '18F/identity-hostdata', branch: 'master' gem 'json-jwt' gem 'local_time' gem 'lograge' +gem 'maxminddb' gem 'net-sftp' gem 'newrelic_rpm' gem 'pg' diff --git a/Gemfile.lock b/Gemfile.lock index 92e177232b4..356e8cf6d19 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -347,6 +347,7 @@ GEM systemu (~> 2.6.2) mail (2.7.1) mini_mime (>= 0.1.1) + maxminddb (0.1.22) memory_profiler (0.9.11) method_source (0.9.2) mime-types (3.2.2) @@ -710,6 +711,7 @@ DEPENDENCIES lexisnexis! local_time lograge + maxminddb net-sftp newrelic_rpm overcommit @@ -768,7 +770,7 @@ DEPENDENCIES zxcvbn-js RUBY VERSION - ruby 2.5.1p57 + ruby 2.5.3p105 BUNDLED WITH - 1.16.6 + 1.17.1 diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 0216afbab51..11b93aaef94 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -13,6 +13,8 @@ def show personal_key: flash[:personal_key], decorated_user: current_user.decorate ) + + @login_presenter = LoginPresenter.new(user: current_user) end private diff --git a/app/controllers/analytics_controller.rb b/app/controllers/analytics_controller.rb index 87bd97f62f6..866b616e704 100644 --- a/app/controllers/analytics_controller.rb +++ b/app/controllers/analytics_controller.rb @@ -20,22 +20,21 @@ def results def platform_authenticator_result return unless current_user - return if platform_authenticator_results_saved? || !platform_authenticator_params_valid? + return if platform_authenticator_results_saved? || platform_authenticator_available?.nil? session[:platform_authenticator_analytics_saved] = true - platform_authenticator_available = params[:available] || - params.dig(:platform_authenticator, :available) - extra = { platform_authenticator: (platform_authenticator_available == 'true') } + extra = { platform_authenticator: platform_authenticator_available? } FormResponse.new(success: true, errors: {}, extra: extra) end - def platform_authenticator_params_valid? - result = params[:available] || params.dig(:platform_authenticator, :available) - %w[true false].include?(result) + def platform_authenticator_available? + @platform_authenticator_available ||= begin + available = params.dig(:platform_authenticator, :available) + available == 'true' if %w[true false].include?(available) + end end def platform_authenticator_results_saved? - session[:platform_authenticator_analytics_saved] == true || - session[:platform_authenticator] == true + session[:platform_authenticator_analytics_saved] == true end end diff --git a/app/controllers/sign_up/cancellations_controller.rb b/app/controllers/sign_up/cancellations_controller.rb new file mode 100644 index 00000000000..e363212d3ff --- /dev/null +++ b/app/controllers/sign_up/cancellations_controller.rb @@ -0,0 +1,21 @@ +module SignUp + class CancellationsController < ApplicationController + before_action :ensure_in_setup + + def new + properties = ParseControllerFromReferer.new(request.referer).call + analytics.track_event(Analytics::USER_REGISTRATION_CANCELLATION, properties) + @presenter = CancellationPresenter.new(view_context: view_context) + end + + private + + def ensure_in_setup + redirect_to root_url if !session[:user_confirmation_token] && two_factor_enabled + end + + def two_factor_enabled + current_user && MfaPolicy.new(current_user).two_factor_enabled? + end + end +end diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb index e248d9c117c..cc8dc881e7b 100644 --- a/app/controllers/two_factor_authentication/otp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb @@ -12,9 +12,10 @@ def show end def create - result = OtpVerificationForm.new(current_user, form_params[:code].strip).submit + result = OtpVerificationForm.new(current_user, sanitized_otp_code).submit + properties = result.to_h.merge(analytics_properties) - analytics.track_event(Analytics::MULTI_FACTOR_AUTH, result.to_h.merge(analytics_properties)) + analytics.track_event(mfa_event_name, properties) if result.success? handle_valid_otp @@ -62,10 +63,20 @@ def phone user_session[:unconfirmed_phone] end + def sanitized_otp_code + form_params[:code].strip + end + def form_params params.permit(:code) end + def mfa_event_name + return Analytics::MULTI_FACTOR_AUTH_SETUP if context == 'confirmation' + + Analytics::MULTI_FACTOR_AUTH + end + def analytics_properties { context: context, diff --git a/app/controllers/users/piv_cac_authentication_setup_controller.rb b/app/controllers/users/piv_cac_authentication_setup_controller.rb index 31395b8713c..4a251d7ca32 100644 --- a/app/controllers/users/piv_cac_authentication_setup_controller.rb +++ b/app/controllers/users/piv_cac_authentication_setup_controller.rb @@ -53,7 +53,7 @@ def two_factor_enabled? def process_piv_cac_setup result = user_piv_cac_form.submit - analytics.track_event(Analytics::USER_REGISTRATION_PIV_CAC_ENABLED, result.to_h) + analytics.track_event(Analytics::MULTI_FACTOR_AUTH_SETUP, result.to_h) if result.success? process_valid_submission else @@ -97,7 +97,8 @@ def process_invalid_submission end def authorize_piv_cac_disable - redirect_to account_url unless piv_cac_enabled? + return redirect_to account_url unless piv_cac_enabled? && + MfaPolicy.new(current_user).multiple_factors_enabled? end def authorize_piv_cac_setup diff --git a/app/controllers/users/totp_setup_controller.rb b/app/controllers/users/totp_setup_controller.rb index ce49317be2d..28c327eee40 100644 --- a/app/controllers/users/totp_setup_controller.rb +++ b/app/controllers/users/totp_setup_controller.rb @@ -16,7 +16,7 @@ def new def confirm result = TotpSetupForm.new(current_user, new_totp_secret, params[:code].strip).submit - analytics.track_event(Analytics::TOTP_SETUP, result.to_h) + analytics.track_event(Analytics::MULTI_FACTOR_AUTH_SETUP, result.to_h) if result.success? process_valid_code @@ -26,11 +26,8 @@ def confirm end def disable - if current_user.totp_enabled? - analytics.track_event(Analytics::TOTP_USER_DISABLED) - create_user_event(:authenticator_disabled) - UpdateUser.new(user: current_user, attributes: { otp_secret_key: nil }).call - flash[:success] = t('notices.totp_disabled') + if current_user.totp_enabled? && MfaPolicy.new(current_user).multiple_factors_enabled? + process_successful_disable end redirect_to account_url end @@ -60,6 +57,13 @@ def process_valid_code user_session.delete(:new_totp_secret) end + def process_successful_disable + analytics.track_event(Analytics::TOTP_USER_DISABLED) + create_user_event(:authenticator_disabled) + UpdateUser.new(user: current_user, attributes: { otp_secret_key: nil }).call + flash[:success] = t('notices.totp_disabled') + end + def mark_user_as_fully_authenticated user_session[TwoFactorAuthentication::NEED_AUTHENTICATION] = false user_session[:authn_at] = Time.zone.now diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index 24ee92ad16f..0a84bcfc580 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -14,7 +14,7 @@ def new def confirm form = WebauthnSetupForm.new(current_user, user_session) result = form.submit(request.protocol, params) - analytics.track_event(Analytics::WEBAUTHN_SETUP_SUBMITTED, result.to_h) + analytics.track_event(Analytics::MULTI_FACTOR_AUTH_SETUP, result.to_h) if result.success? process_valid_webauthn else diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index dbc27e88ecb..3709c921c3e 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,4 +1,6 @@ class UsersController < ApplicationController + before_action :ensure_in_setup + def destroy track_account_deletion_event url_after_cancellation = decorated_session.cancel_link_url @@ -19,4 +21,12 @@ def destroy_user user&.destroy! sign_out if user end + + def ensure_in_setup + redirect_to root_url if !session[:user_confirmation_token] && two_factor_enabled + end + + def two_factor_enabled + current_user && MfaPolicy.new(current_user).two_factor_enabled? + end end diff --git a/app/forms/totp_setup_form.rb b/app/forms/totp_setup_form.rb index f895be2385f..8fcdd7d6e0a 100644 --- a/app/forms/totp_setup_form.rb +++ b/app/forms/totp_setup_form.rb @@ -29,6 +29,9 @@ def process_valid_submission end def extra_analytics_attributes - { totp_secret_present: secret.present? } + { + totp_secret_present: secret.present?, + multi_factor_auth_method: 'totp', + } end end diff --git a/app/forms/user_piv_cac_setup_form.rb b/app/forms/user_piv_cac_setup_form.rb index 52e98dd8d3c..f6a6b897c4c 100644 --- a/app/forms/user_piv_cac_setup_form.rb +++ b/app/forms/user_piv_cac_setup_form.rb @@ -10,7 +10,11 @@ class UserPivCacSetupForm def submit success = valid? && valid_token? - FormResponse.new(success: success && process_valid_submission, errors: {}) + FormResponse.new( + success: success && process_valid_submission, + errors: {}, + extra: extra_analytics_attributes + ) end private @@ -76,4 +80,10 @@ def user_has_no_piv_cac true end end + + def extra_analytics_attributes + { + multi_factor_auth_method: 'piv_cac', + } + end end diff --git a/app/forms/webauthn_setup_form.rb b/app/forms/webauthn_setup_form.rb index 776e2498a04..1b6b6f33920 100644 --- a/app/forms/webauthn_setup_form.rb +++ b/app/forms/webauthn_setup_form.rb @@ -82,6 +82,9 @@ def create_user_event end def extra_analytics_attributes - { mfa_method_counts: MfaContext.new(user).enabled_two_factor_configuration_counts_hash } + { + mfa_method_counts: MfaContext.new(user).enabled_two_factor_configuration_counts_hash, + multi_factor_auth_method: 'webauthn', + } end end diff --git a/app/models/identity.rb b/app/models/identity.rb index 3ed9bb5f986..73f7aa6f19e 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -29,8 +29,8 @@ def decorate end def piv_cac_available? - PivCacService.piv_cac_available_for_agency?( - sp_metadata[:agency], + PivCacService.piv_cac_available_for_sp?( + ServiceProvider.from_issuer(service_provider), user.email_addresses.map(&:email) ) end diff --git a/app/models/service_provider.rb b/app/models/service_provider.rb index e4658cd443a..5bd83e74c8d 100644 --- a/app/models/service_provider.rb +++ b/app/models/service_provider.rb @@ -43,7 +43,7 @@ def live? end def piv_cac_available?(user = nil) - PivCacService.piv_cac_available_for_agency?(agency, user&.email_addresses&.map(&:email)) + PivCacService.piv_cac_available_for_sp?(self, user&.email_addresses&.map(&:email)) end private diff --git a/app/presenters/cancellation_presenter.rb b/app/presenters/cancellation_presenter.rb new file mode 100644 index 00000000000..2194a4d9495 --- /dev/null +++ b/app/presenters/cancellation_presenter.rb @@ -0,0 +1,48 @@ +class CancellationPresenter < FailurePresenter + include ActionView::Helpers::TranslationHelper + include Rails.application.routes.url_helpers + + delegate :request, to: :view_context + + attr_reader :view_context + + def initialize(view_context:) + super(:warning) + @view_context = view_context + end + + def title + t('headings.cancellations.prompt') + end + + def header + t('headings.cancellations.prompt') + end + + def cancellation_warnings + [ + t('users.delete.bullet_1', app: APP_NAME), + t('users.delete.bullet_2_loa1'), + t('users.delete.bullet_3', app: APP_NAME), + ] + end + + def go_back_path + referer_path || two_factor_options_path + end + + private + + def referer_path + referer_string = request.env['HTTP_REFERER'] + return if referer_string.blank? + referer_uri = URI.parse(referer_string) + return if referer_uri.scheme == 'javascript' + return unless referer_uri.host == Figaro.env.domain_name.split(':')[0] + extract_path_and_query_from_uri(referer_uri) + end + + def extract_path_and_query_from_uri(uri) + [uri.path, uri.query].compact.join('?') + end +end diff --git a/app/presenters/login_presenter.rb b/app/presenters/login_presenter.rb new file mode 100644 index 00000000000..9b7095bbee4 --- /dev/null +++ b/app/presenters/login_presenter.rb @@ -0,0 +1,59 @@ +class LoginPresenter + include ActionView::Helpers::DateHelper + + def initialize(user:) + @user = user + end + + def current_sign_in_location_and_ip + I18n.t('account.index.sign_in_location_and_ip', location: current_location, ip: current_ip) + end + + def last_sign_in_location_and_ip + I18n.t('account.index.sign_in_location_and_ip', location: last_location, ip: last_ip) + end + + def current_timestamp + timestamp = user.current_sign_in_at || Time.zone.now + I18n.t( + 'account.index.sign_in_timestamp', + timestamp: time_ago_in_words( + timestamp, highest_measures: 2, two_words_connector: two_words_connector + ) + ) + end + + def last_timestamp + timestamp = user.last_sign_in_at || Time.zone.now + I18n.t( + 'account.index.sign_in_timestamp', + timestamp: time_ago_in_words( + timestamp, highest_measures: 2, two_words_connector: two_words_connector + ) + ) + end + + private + + attr_reader :user + + def current_location + IpGeocoder.new(current_ip).location + end + + def last_location + IpGeocoder.new(last_ip).location + end + + def current_ip + user.current_sign_in_ip + end + + def last_ip + user.last_sign_in_ip + end + + def two_words_connector + " #{I18n.t('datetime.dotiw.two_words_connector')} " + end +end diff --git a/app/services/account_reset/validate_cancel_token.rb b/app/services/account_reset/validate_cancel_token.rb index 906cfdf9d23..66d79d0ed65 100644 --- a/app/services/account_reset/validate_cancel_token.rb +++ b/app/services/account_reset/validate_cancel_token.rb @@ -23,7 +23,7 @@ def user def extra_analytics_attributes { - event: 'visit', + event: 'cancel token validation', user_id: user.uuid, } end diff --git a/app/services/analytics.rb b/app/services/analytics.rb index fbd816d7d66..3524443b719 100644 --- a/app/services/analytics.rb +++ b/app/services/analytics.rb @@ -100,6 +100,7 @@ def browser MULTI_FACTOR_AUTH_OPTION_LIST_VISIT = 'Multi-Factor Authentication: option list visited'.freeze MULTI_FACTOR_AUTH_PHONE_SETUP = 'Multi-Factor Authentication: phone setup'.freeze MULTI_FACTOR_AUTH_MAX_SENDS = 'Multi-Factor Authentication: max otp sends reached'.freeze + MULTI_FACTOR_AUTH_SETUP = 'Multi-Factor Authentication Setup'.freeze OPENID_CONNECT_BEARER_TOKEN = 'OpenID Connect: bearer token authentication'.freeze OPENID_CONNECT_REQUEST_AUTHORIZATION = 'OpenID Connect: authorization request'.freeze OPENID_CONNECT_TOKEN = 'OpenID Connect: token'.freeze @@ -121,7 +122,6 @@ def browser SAML_AUTH = 'SAML Auth'.freeze SESSION_TIMED_OUT = 'Session Timed Out'.freeze SIGN_IN_PAGE_VISIT = 'Sign in page visited'.freeze - TOTP_SETUP = 'TOTP Setup'.freeze TOTP_SETUP_VISIT = 'TOTP Setup Visited'.freeze TOTP_USER_DISABLED = 'TOTP: User Disabled TOTP'.freeze TWILIO_PHONE_VALIDATION_FAILED = 'Twilio Phone Validation Failed'.freeze @@ -129,6 +129,7 @@ def browser TWILIO_SMS_INBOUND_MESSAGE_VALIDATION_FAILED = 'Twilio SMS Inbound Validation Failed'.freeze USER_REGISTRATION_AGENCY_HANDOFF_PAGE_VISIT = 'User registration: agency handoff visited'.freeze USER_REGISTRATION_AGENCY_HANDOFF_COMPLETE = 'User registration: agency handoff complete'.freeze + USER_REGISTRATION_CANCELLATION = 'User registration: cancellation visited'.freeze USER_REGISTRATION_EMAIL = 'User Registration: Email Submitted'.freeze USER_REGISTRATION_EMAIL_CONFIRMATION = 'User Registration: Email Confirmation'.freeze USER_REGISTRATION_EMAIL_CONFIRMATION_RESEND = 'User Registration: Email Confirmation requested due to invalid token'.freeze @@ -139,10 +140,8 @@ def browser USER_REGISTRATION_PHONE_SETUP_VISIT = 'User Registration: phone setup visited'.freeze USER_REGISTRATION_PERSONAL_KEY_VISIT = 'User Registration: personal key visited'.freeze USER_REGISTRATION_PIV_CAC_DISABLED = 'User Registration: piv cac disabled'.freeze - USER_REGISTRATION_PIV_CAC_ENABLED = 'User Registration: piv cac enabled'.freeze USER_REGISTRATION_PIV_CAC_SETUP_VISIT = 'User Registration: piv cac setup visited'.freeze WEBAUTHN_DELETED = 'WebAuthn Deleted'.freeze WEBAUTHN_SETUP_VISIT = 'WebAuthn Setup Visited'.freeze - WEBAUTHN_SETUP_SUBMITTED = 'WebAuthn Setup Submitted'.freeze # rubocop:enable Metrics/LineLength end diff --git a/app/services/ip_geocoder.rb b/app/services/ip_geocoder.rb new file mode 100644 index 00000000000..69fb112d6a4 --- /dev/null +++ b/app/services/ip_geocoder.rb @@ -0,0 +1,42 @@ +class IpGeocoder + def initialize(ip) + @ip = ip + end + + def location + geocoded_location&.language = I18n.locale + + return city_and_state if both_city_and_state_present? + return country if country.present? + + I18n.t('account.index.unknown_location') + end + + private + + attr_reader :ip + + def city_and_state + "#{city}, #{state}" + end + + def both_city_and_state_present? + city.present? && state.present? + end + + def city + geocoded_location&.city + end + + def state + geocoded_location&.state_code + end + + def country + geocoded_location&.country + end + + def geocoded_location + @geocoded_location ||= Geocoder.search(ip).first + end +end diff --git a/app/services/piv_cac_service.rb b/app/services/piv_cac_service.rb index 45eda21533b..f075e954dec 100644 --- a/app/services/piv_cac_service.rb +++ b/app/services/piv_cac_service.rb @@ -28,20 +28,14 @@ def piv_cac_verify_token_link Figaro.env.piv_cac_verify_token_url end - def piv_cac_available_for_agency?(agency, emails = []) - available_for_agency?(agency) || available_for_email?(agency, emails) + def piv_cac_available_for_sp?(sp, emails = []) + sp.piv_cac || available_for_email?(sp, emails) end private - def available_for_agency?(agency) - return if agency.blank? - piv_cac_agencies = JSON.parse(Figaro.env.piv_cac_agencies || '[]') - piv_cac_agencies.include?(agency) - end - - def available_for_email?(agency, emails) - return unless emails.any? && agency_scoped_by_email?(agency) + def available_for_email?(sp, emails) + return unless emails.any? && sp.piv_cac_scoped_by_email piv_cac_email_domains = Figaro.env.piv_cac_email_domains || '[]' supported_domains = JSON.parse(piv_cac_email_domains) @@ -51,15 +45,6 @@ def available_for_email?(agency, emails) emails_match_domains?(email_domains, supported_domains) end - def agency_scoped_by_email?(agency) - return if agency.blank? - - piv_cac_agencies_email_scope = - JSON.parse(Figaro.env.piv_cac_agencies_scoped_by_email || '[]') - - piv_cac_agencies_email_scope.include?(agency) - end - def emails_match_domains?(email_domains, supported_domains) partial_domains, exact_domains = supported_domains.partition { |domain| domain[0] == '.' } @@ -79,12 +64,6 @@ def randomize_uri(uri) uri.gsub('{random}') { |_| SecureRandom.hex(RANDOM_HOSTNAME_BYTES) } end - # Only used in tests - def reset_piv_cac_avaialable_agencies - @piv_cac_agencies = nil - @piv_cac_agencies_email_scope = nil - end - def token_present(token) raise ArgumentError, 'token missing' if token.blank? true diff --git a/app/view_models/account_show.rb b/app/view_models/account_show.rb index 1909a4c9a31..ea454079ade 100644 --- a/app/view_models/account_show.rb +++ b/app/view_models/account_show.rb @@ -58,20 +58,38 @@ def pii_partial def totp_partial if TwoFactorAuthentication::AuthAppPolicy.new(decorated_user.user).enabled? - 'accounts/actions/disable_totp' + disable_totp_partial else - 'accounts/actions/enable_totp' + enable_totp_partial end end + def disable_totp_partial + return 'shared/null' unless MfaPolicy.new(decorated_user.user).multiple_factors_enabled? + 'accounts/actions/disable_totp' + end + + def enable_totp_partial + 'accounts/actions/enable_totp' + end + def piv_cac_partial if TwoFactorAuthentication::PivCacPolicy.new(decorated_user.user).enabled? - 'accounts/actions/disable_piv_cac' + disable_piv_cac_partial else - 'accounts/actions/enable_piv_cac' + enable_piv_cac_partial end end + def disable_piv_cac_partial + return 'shared/null' unless MfaPolicy.new(decorated_user.user).multiple_factors_enabled? + 'accounts/actions/disable_piv_cac' + end + + def enable_piv_cac_partial + 'accounts/actions/enable_piv_cac' + end + def manage_personal_key_partial yield if decorated_user.password_reset_profile.blank? end diff --git a/app/views/accounts/_account_item.html.slim b/app/views/accounts/_account_item.html.slim index f88880673e7..bfd4e4f635e 100644 --- a/app/views/accounts/_account_item.html.slim +++ b/app/views/accounts/_account_item.html.slim @@ -7,5 +7,5 @@ .col.col-4.right-align - if local_assigns.key? :path = render action, path: path, name: name - - else + - elsif local_assigns.key? :action = render action diff --git a/app/views/accounts/_current_sign_in.html.slim b/app/views/accounts/_current_sign_in.html.slim new file mode 100644 index 00000000000..5ec766431e8 --- /dev/null +++ b/app/views/accounts/_current_sign_in.html.slim @@ -0,0 +1,6 @@ +.p2.clearfix.border-top + .clearfix.mxn1 + .sm-col.sm-col-6.px1 + .bold = presenter.current_sign_in_location_and_ip + .sm-col.sm-col-6.px1.sm-right-align + = presenter.current_timestamp diff --git a/app/views/accounts/_last_sign_in.html.slim b/app/views/accounts/_last_sign_in.html.slim new file mode 100644 index 00000000000..357aee8ce99 --- /dev/null +++ b/app/views/accounts/_last_sign_in.html.slim @@ -0,0 +1,6 @@ +.p2.clearfix.border-top + .clearfix.mxn1 + .sm-col.sm-col-6.px1 + .bold = presenter.last_sign_in_location_and_ip + .sm-col.sm-col-6.px1.sm-right-align + = presenter.last_timestamp diff --git a/app/views/accounts/show.html.slim b/app/views/accounts/show.html.slim index 5dfde7d798e..5c542b0dd49 100644 --- a/app/views/accounts/show.html.slim +++ b/app/views/accounts/show.html.slim @@ -74,6 +74,13 @@ h1.hide = t('titles.account') - @view_model.recent_events.each do |event| = render event.event_partial, event: event +.mb3.profile-info-box + .bg-lightest-blue.pb1.pt1.px2.h6.caps.clearfix + = t('headings.account.login_history') + = image_tag asset_url('history.svg'), width: 12, class: 'ml1' + = render 'accounts/current_sign_in', presenter: @login_presenter + = render 'accounts/last_sign_in', presenter: @login_presenter + .mb3.profile-info-box .bg-lightest-blue.pb1.pt1.px2.h6.caps.clearfix = t('headings.account.account_management') diff --git a/app/views/shared/_cancel.html.slim b/app/views/shared/_cancel.html.slim index f5197c8b927..fb32d5866a7 100644 --- a/app/views/shared/_cancel.html.slim +++ b/app/views/shared/_cancel.html.slim @@ -1,12 +1,8 @@ -- cancel = sign_up_or_idv_no_js_link || link - other_cancel = link || decorated_session&.sp_return_url || profile_path .mt2.pt1.border-top - if user_signing_up? - - method = user_signing_up? ? :delete : :get - - = button_to cancel_link_text, cancel, method: method, - class: 'btn btn-link', id: 'auth-flow-cancel' + = button_to cancel_link_text, sign_up_cancel_path, method: :get, class: 'btn btn-link' = render 'shared/cancel_action_modal', idv: user_verifying_identity?, user_signing_up: user_signing_up? diff --git a/app/views/sign_up/cancellations/new.html.slim b/app/views/sign_up/cancellations/new.html.slim new file mode 100644 index 00000000000..e92791c1103 --- /dev/null +++ b/app/views/sign_up/cancellations/new.html.slim @@ -0,0 +1,12 @@ += render 'shared/failure', presenter: @presenter + +p.mb1.bold = t('sign_up.cancel.warning_header') + +ul class="list-reset #{@presenter.state_color}-dots" + - @presenter.cancellation_warnings.each do |warning| + li = warning + +.mt3 = button_to t('forms.buttons.cancel'), destroy_user_path, + method: :delete, class: 'btn btn-primary sm-col-6 col-12 btn-wide' +.mt2 = link_to t('links.go_back'), @presenter.go_back_path, + class: 'btn btn-secondary sm-col-6 col-12 btn-wide' diff --git a/bin/setup b/bin/setup index a02271c8925..c566fd38f2f 100755 --- a/bin/setup +++ b/bin/setup @@ -35,6 +35,12 @@ Dir.chdir APP_ROOT do run "test -L certs/saml.crt || cp certs/saml.crt.example certs/saml.crt" run "test -L certs/saml2018.crt || cp certs/saml2018.crt.example certs/saml2018.crt" + puts "== Copying GeoLite2 City database ==" + run "test -L geo_data/GeoLite2-City.mmdb || mkdir -p geo_data && cd geo_data && " \ + "curl http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz | tar xvz" + geo_data_file = Dir['geo_data/GeoLite2-City_*/GeoLite2-City.mmdb'].sort.last + run "mv #{geo_data_file} geo_data/GeoLite2-City.mmdb" + if ARGV.shift == "--docker" then run 'docker-compose build' run 'docker-compose run --rm web yarn install' diff --git a/config/application.yml.example b/config/application.yml.example index 91f15b899fb..845916b863c 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -149,7 +149,6 @@ development: otp_valid_for: '10' password_pepper: 'f22d4b2cafac9066fe2f4416f5b7a32c' password_strength_enabled: 'true' - piv_cac_agencies: '["Test Government Agency"]' piv_cac_email_domains: '[".mil","state.gov"]' piv_cac_verify_token_secret: 'ee7f20f44cdc2ba0c6830f70470d1d1d059e1279cdb58134db92b35947b1528ef5525ece5910cf4f2321ab989a618feea12ef95711dbc62b9601e8520a34ee12' piv_cac_service_url: 'https://localhost:8443/' @@ -275,8 +274,6 @@ production: participate_in_dap: 'false' # pair with google_analytics_key password_pepper: # generate via `rake secret` password_strength_enabled: 'true' - piv_cac_agencies: '["DOD","NGA","USDS"]' - piv_cac_agencies_scoped_by_email: '["GSA"]' piv_cac_email_domains: '[".mil","state.gov"]' pkcs11_lib: '/opt/cloudhsm/lib/libcloudhsm_pkcs11.so' platform_authenticator_analytics_enabled: 'true' @@ -396,7 +393,6 @@ test: otp_valid_for: '10' password_pepper: 'f22d4b2cafac9066fe2f4416f5b7a32c' password_strength_enabled: 'false' - piv_cac_agencies: '["Test Government Agency"]' piv_cac_email_domains: '[".mil","state.gov"]' piv_cac_service_url: 'https://localhost:8443/' piv_cac_verify_token_secret: '3ac13bfa23e22adae321194c083e783faf89469f6f85dcc0802b27475c94b5c3891b5657bd87d0c1ad65de459166440512f2311018db90d57b15d8ab6660748f' diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index e9eb1f52038..8e24811dab9 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -97,6 +97,7 @@ ignore_unused: - 'devise.mailer.confirmation_instructions.subject' - 'devise.mailer.reset_password_instructions.subject' - 'devise.sessions.signed_in' + - 'datetime.dotiw.two_words_connector' - 'service_providers.*' - 'two_factor_authentication.invalid_otp' - 'two_factor_authentication.invalid_personal_key' diff --git a/config/initializers/geocoder.rb b/config/initializers/geocoder.rb new file mode 100644 index 00000000000..4d09553bdbc --- /dev/null +++ b/config/initializers/geocoder.rb @@ -0,0 +1,6 @@ +Geocoder.configure( + ip_lookup: :geoip2, + geoip2: { + file: Rails.root.join('geo_data', 'GeoLite2-City.mmdb'), + } +) diff --git a/config/locales/account/en.yml b/config/locales/account/en.yml index da29c9b4249..16300822c91 100644 --- a/config/locales/account/en.yml +++ b/config/locales/account/en.yml @@ -21,7 +21,10 @@ en: reactivation: instructions: Your profile was recently deactivated due to a password reset. link: Reactivate your profile now. + sign_in_location_and_ip: 'From %{location} (IP address: %{ip})' + sign_in_timestamp: "%{timestamp} ago" ssn: Social Security Number + unknown_location: unknown location verification: instructions: Your account requires a secret code to be verified. reactivate_button: Enter the code you received via US mail diff --git a/config/locales/account/es.yml b/config/locales/account/es.yml index 3407a9bc55e..2fd852e29c8 100644 --- a/config/locales/account/es.yml +++ b/config/locales/account/es.yml @@ -21,7 +21,10 @@ es: reactivation: instructions: Su perfil ha sido desactivado debido a un cambio de contraseña. link: Reactive su perfil ahora. + sign_in_location_and_ip: 'Desde %{location} (Dirección IP: %{ip})' + sign_in_timestamp: Hace %{timestamp} ssn: Número de Seguro Social + unknown_location: ubicación desconocida verification: instructions: Su cuenta requiere que un código secreto sea verificado. reactivate_button: Ingrese el código que recibió por correo postal. diff --git a/config/locales/account/fr.yml b/config/locales/account/fr.yml index fff3e00bca9..e313b0e8714 100644 --- a/config/locales/account/fr.yml +++ b/config/locales/account/fr.yml @@ -23,7 +23,10 @@ fr: de mot passe. Vous pouvez utiliser votre clé personnelle pour réactiver votre profil. link: Réactivez votre profil maintenant. + sign_in_location_and_ip: "%{location} (Adresse IP: %{ip})" + sign_in_timestamp: Il y a %{timestamp} ssn: Numéro d'assurance sociale + unknown_location: lieu inconnu verification: instructions: Votre compte requiert la vérification d'un code secret. reactivate_button: Entrez le code que vous avez reçu par la poste diff --git a/config/locales/dotiw/en.yml b/config/locales/dotiw/en.yml new file mode 100644 index 00000000000..f5c277acf76 --- /dev/null +++ b/config/locales/dotiw/en.yml @@ -0,0 +1,5 @@ +--- +en: + datetime: + dotiw: + two_words_connector: and diff --git a/config/locales/dotiw/es.yml b/config/locales/dotiw/es.yml new file mode 100644 index 00000000000..0c407bd25c2 --- /dev/null +++ b/config/locales/dotiw/es.yml @@ -0,0 +1,5 @@ +--- +es: + datetime: + dotiw: + two_words_connector: y diff --git a/config/locales/dotiw/fr.yml b/config/locales/dotiw/fr.yml new file mode 100644 index 00000000000..d435f590725 --- /dev/null +++ b/config/locales/dotiw/fr.yml @@ -0,0 +1,5 @@ +--- +fr: + datetime: + dotiw: + two_words_connector: et diff --git a/config/locales/headings/en.yml b/config/locales/headings/en.yml index 9f733dfd293..62c26156c5b 100644 --- a/config/locales/headings/en.yml +++ b/config/locales/headings/en.yml @@ -5,6 +5,7 @@ en: account_history: Account history account_management: Account Management connected_apps: Applications + login_history: Login history login_info: Your account profile_info: Profile information reactivate: Reactivate your account diff --git a/config/locales/headings/es.yml b/config/locales/headings/es.yml index 92a0f466cfd..6b346b5b369 100644 --- a/config/locales/headings/es.yml +++ b/config/locales/headings/es.yml @@ -5,6 +5,7 @@ es: account_history: Historial de cuenta account_management: Manejo de cuenta connected_apps: Aplicaciones + login_history: Historial de sesión login_info: Su cuenta profile_info: Información de perfil reactivate: Reactive su cuenta diff --git a/config/locales/headings/fr.yml b/config/locales/headings/fr.yml index ad9ae7794ce..179ba2d77bc 100644 --- a/config/locales/headings/fr.yml +++ b/config/locales/headings/fr.yml @@ -5,6 +5,7 @@ fr: account_history: Historique du compte account_management: Gestion de compte connected_apps: Applications + login_history: Historique de connexion login_info: Votre compte profile_info: Information du profil reactivate: Réactivez votre compte diff --git a/config/locales/users/en.yml b/config/locales/users/en.yml index eb31ef6245b..3252223b891 100644 --- a/config/locales/users/en.yml +++ b/config/locales/users/en.yml @@ -5,9 +5,8 @@ en: actions: cancel: Cancel and return to your profile delete: Delete account - bullet_1: You won't have a %{app} account. - bullet_2_loa1: "%{app} will delete your email address, password, and phone number - from our system." + bullet_1: You won't have a %{app} account + bullet_2_loa1: We'll delete your email address, password, and phone number bullet_2_loa3: "%{app} will delete your email address, password, phone number, name, address, date of birth and Social Security number from our system." bullet_3: You won't be able to securely access your information using %{app}. diff --git a/config/locales/users/es.yml b/config/locales/users/es.yml index 0d618b1fb5e..bca4320f6b5 100644 --- a/config/locales/users/es.yml +++ b/config/locales/users/es.yml @@ -6,8 +6,8 @@ es: cancel: Anular y regresar a su perfil delete: Eliminar cuenta bullet_1: Usted no tendrá una %{app} cuenta. - bullet_2_loa1: "%{app} borrará su email, contraseña y número de teléfono de - nuestro sistema." + bullet_2_loa1: Eliminaremos su dirección de correo electrónico, contraseña y + número de teléfono bullet_2_loa3: "%{app} borrará su email, contraseña, número de teléfono, nombre, dirección, fecha de nacimiento y número de Seguro Social de nuestro sistema." bullet_3: Usted no podrá tener acceso seguro a su información usando %{app} diff --git a/config/locales/users/fr.yml b/config/locales/users/fr.yml index 9a56406bf80..ccae566f818 100644 --- a/config/locales/users/fr.yml +++ b/config/locales/users/fr.yml @@ -6,8 +6,8 @@ fr: cancel: Annuler et retourner à votre profil delete: Supprimer le compte bullet_1: Vous n'aurez pas de compte %{app}. - bullet_2_loa1: "%{app} supprimera votre adresse e-mail, votre mot de passe et - votre numéro de téléphone de notre système." + bullet_2_loa1: Nous effacerons votre adresse email, votre mot de passe et votre + numéro de téléphone bullet_2_loa3: "%{app} supprimera votre adresse e-mail, votre mot de passe et votre numéro de téléphone, votre nom, votre adresse, votre date de naissance et votre numéro de sécurité sociale de notre système." diff --git a/config/routes.rb b/config/routes.rb index d08c590d510..7392a243d92 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -173,6 +173,8 @@ get '/sign_up/verify_email' => 'sign_up/emails#show', as: :sign_up_verify_email get '/sign_up/completed' => 'sign_up/completions#show', as: :sign_up_completed post '/sign_up/completed' => 'sign_up/completions#update' + get '/sign_up/cancel/' => 'sign_up/cancellations#new', as: :sign_up_cancel + delete '/sign_up/cancel' => 'sign_up/cancellations#destroy' match '/sign_out' => 'sign_out#destroy', via: %i[get post delete] diff --git a/config/service_providers.yml b/config/service_providers.yml index 6fe9e972d2a..7c6b8000376 100644 --- a/config/service_providers.yml +++ b/config/service_providers.yml @@ -499,6 +499,7 @@ production: - 'https://training.vgihub.geointservices.io/op_redirect' - 'https://wiki.vgihub.geointservices.io/op_redirect' restrict_to_deploy_env: 'prod' + piv_cac: true # NGA GEOWorks Landing Page 'urn:gov:gsa:openidconnect.profiles:sp:sso:nga:landingpage': @@ -517,6 +518,7 @@ production: - 'https://nga-geoworks.io/auth/login-gov/callback/loa-1' - 'https://ngageoworks.io/auth/login-gov/callback/loa-1' restrict_to_deploy_env: 'prod' + piv_cac: true # NGA GEOINT Viewer 'urn:gov:gsa:openidconnect.profiles:sp:sso:nga:geoint_viewer': @@ -534,6 +536,7 @@ production: - 'https://gv.nga-geoworks.io/protected/callback' - 'https://gv.ngageoworks.io/protected/callback' restrict_to_deploy_env: 'prod' + piv_cac: true # NGA HiPER CLOUD 'urn:gov:gsa:openidconnect.profiles:sp:sso:nga:hiper_look': @@ -559,6 +562,7 @@ production: - 'https://hiperlook.nga-geoworks.com/auth_redirect' - 'https://hiperlook.nga-geoworks.com:443/auth_redirect' restrict_to_deploy_env: 'prod' + piv_cac: true # NGA MAGE 'urn:gov:gsa:openidconnect.profiles:sp:sso:nga:mage': @@ -576,6 +580,7 @@ production: - 'https://mage.nga-geoworks.io/auth/login-gov/callback/loa-1' - 'https://mage.ngageoworks.io/auth/login-gov/callback/loa-1' restrict_to_deploy_env: 'prod' + piv_cac: true # DOT 'urn:gov:gsa:openidconnect.profiles:sp:sso:dot:login': @@ -605,6 +610,7 @@ production: - 'https://symphony.nga-geoworks.com/guacamole/#/' - 'https://symphony.nga-geoworks.com/secured' restrict_to_deploy_env: 'prod' + piv_cac: true # Secret Service PIX 'urn:gov:gsa:SAML:2.0.profiles:sp:sso:usss:pix': @@ -638,6 +644,7 @@ production: attribute_bundle: - x509_subject - x509_presented + piv_cac: true # My Move.mil 'urn:gov:gsa:openidconnect.profiles:sp:sso:dod:mymovemilprod': @@ -656,6 +663,7 @@ production: attribute_bundle: - x509_subject - x509_presented + piv_cac: true # DOT – National Registry of Certified Medical Examiners App 'urn:gov:dot:openidconnect.profiles:sp:sso:dot:nr_auth': @@ -686,6 +694,8 @@ production: - 'https://sam.gov/portal/SAM' - 'https://www.sam.gov/portal/SAM' restrict_to_deploy_env: 'prod' + piv_cac: true + piv_cac_scoped_by_email: true # SAM – System for Award Management / testing prod from UAT 'urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:sam_uat': @@ -698,6 +708,8 @@ production: redirect_uris: - 'https://uat.sam.gov/portal/SAM' restrict_to_deploy_env: 'prod' + piv_cac: true + piv_cac_scoped_by_email: true # DOE - Fossil Energy - Import/Export Authorization Portal for Natural Gas 'urn:gov:gsa:openidconnect.profiles:sp:sso:doe:fergas': @@ -740,6 +752,7 @@ production: attribute_bundle: - email restrict_to_deploy_env: 'prod' + piv_cac: true # Forest Service Open Forest Permits 'urn:gov:gsa:open-id-connect:sp:sso:usda-forestservice:epermit-prod': @@ -824,6 +837,7 @@ production: attribute_bundle: - email restrict_to_deploy_env: 'prod' + piv_cac: true # Pre-mod SAM – System for Award Management 'urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:pmsam': @@ -837,6 +851,8 @@ production: - 'https://sam.gov/SAM' - 'https://www.sam.gov/SAM' restrict_to_deploy_env: 'prod' + piv_cac: true + piv_cac_scoped_by_email: true # OPM Secure Portal 'urn:gov:gsa:SAML:2.0.profiles:sp:sso:OPM:TibcoMFT': diff --git a/db/migrate/20181121223714_add_piv_cac_to_service_provider.rb b/db/migrate/20181121223714_add_piv_cac_to_service_provider.rb new file mode 100644 index 00000000000..67824638b69 --- /dev/null +++ b/db/migrate/20181121223714_add_piv_cac_to_service_provider.rb @@ -0,0 +1,14 @@ +class AddPivCacToServiceProvider < ActiveRecord::Migration[5.1] + def up + add_column :service_providers, :piv_cac, :boolean + change_column_default :service_providers, :piv_cac, false + + add_column :service_providers, :piv_cac_scoped_by_email, :boolean + change_column_default :service_providers, :piv_cac_scoped_by_email, false + end + + def down + remove_column :service_providers, :piv_cac + remove_column :service_providers, :piv_cac_scoped_by_email + end +end diff --git a/db/schema.rb b/db/schema.rb index cb124630707..7c160838273 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181029203754) do +ActiveRecord::Schema.define(version: 20181121223714) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -207,6 +207,8 @@ t.text "failure_to_proof_url" t.integer "aal" t.integer "ial" + t.boolean "piv_cac", default: false + t.boolean "piv_cac_scoped_by_email", default: false t.index ["issuer"], name: "index_service_providers_on_issuer", unique: true end diff --git a/lib/deploy/activate.rb b/lib/deploy/activate.rb index a7e710269b7..45805b7aa64 100644 --- a/lib/deploy/activate.rb +++ b/lib/deploy/activate.rb @@ -13,16 +13,49 @@ def initialize(logger: default_logger, s3_client: nil) end def run + download_application_yml_from_s3 + deep_merge_s3_data_with_example_application_yml + set_proper_file_permissions_for_application_yml + + download_geocoding_database_from_s3 + set_proper_file_permissions_for_geolocation_db + end + + private + + def download_application_yml_from_s3 LoginGov::Hostdata.s3(logger: logger, s3_client: s3_client).download_configs( '/%s/idp/v1/application.yml' => env_yaml_path ) + end + def deep_merge_s3_data_with_example_application_yml File.open(result_yaml_path, 'w') { |file| file.puts YAML.dump(application_config) } + end + def set_proper_file_permissions_for_application_yml FileUtils.chmod(0o640, [env_yaml_path, result_yaml_path]) end - private + def download_geocoding_database_from_s3 + ec2_region = ec2_data.region + + LoginGov::Hostdata::S3.new( + bucket: "login-gov.secrets.#{ec2_data.account_id}-#{ec2_region}", + env: nil, + region: ec2_region, + logger: logger, + s3_client: s3_client + ).download_configs('/common/GeoLite2-City.mmdb' => geolocation_db_path) + end + + def ec2_data + @ec2_data ||= LoginGov::Hostdata::EC2.load + end + + def set_proper_file_permissions_for_geolocation_db + FileUtils.chmod(0o644, geolocation_db_path) + end def default_logger logger = Logger.new(STDOUT) @@ -49,5 +82,9 @@ def example_application_yaml_path def result_yaml_path File.join(root, 'config/application.yml') end + + def geolocation_db_path + File.join(root, 'geo_data/GeoLite2-City.mmdb') + end end end diff --git a/spec/controllers/account_reset/cancel_controller_spec.rb b/spec/controllers/account_reset/cancel_controller_spec.rb index 3b853b12e10..611761358a9 100644 --- a/spec/controllers/account_reset/cancel_controller_spec.rb +++ b/spec/controllers/account_reset/cancel_controller_spec.rb @@ -91,7 +91,7 @@ stub_analytics properties = { user_id: 'anonymous-uuid', - event: 'visit', + event: 'cancel token validation', success: false, errors: { token: [t('errors.account_reset.cancel_token_invalid')] }, } diff --git a/spec/controllers/sign_up/cancellations_controller_spec.rb b/spec/controllers/sign_up/cancellations_controller_spec.rb new file mode 100644 index 00000000000..f2001884301 --- /dev/null +++ b/spec/controllers/sign_up/cancellations_controller_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe SignUp::CancellationsController do + describe '#new' do + it 'tracks the event in analytics when referer is nil' do + stub_sign_in + stub_analytics + properties = { request_came_from: 'no referer' } + + expect(@analytics).to receive(:track_event).with( + Analytics::USER_REGISTRATION_CANCELLATION, properties + ) + + get :new + end + + it 'tracks the event in analytics when referer is present' do + stub_sign_in + stub_analytics + request.env['HTTP_REFERER'] = 'http://example.com/' + properties = { request_came_from: 'users/sessions#new' } + + expect(@analytics).to receive(:track_event).with( + Analytics::USER_REGISTRATION_CANCELLATION, properties + ) + + get :new + end + end +end diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb index 2ea0bf80d33..0f03e440a64 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -307,7 +307,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH, properties) + with(Analytics::MULTI_FACTOR_AUTH_SETUP, properties) expect(subject).to have_received(:create_user_event).with(:phone_changed) expect(subject).to have_received(:create_user_event).exactly(:once) subject.current_user.email_addresses.each do |email_address| @@ -352,7 +352,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH, properties) + with(Analytics::MULTI_FACTOR_AUTH_SETUP, properties) end end end @@ -388,7 +388,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::MULTI_FACTOR_AUTH, properties) + with(Analytics::MULTI_FACTOR_AUTH_SETUP, properties) expect(subject).to have_received(:create_user_event).with(:phone_confirmed) expect(subject).to have_received(:create_user_event).exactly(:once) diff --git a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb index ab7ef139bfe..245e0f0a240 100644 --- a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb @@ -129,7 +129,7 @@ end context 'with associated piv/cac' do - let(:user) { create(:user, :with_piv_or_cac) } + let(:user) { create(:user, :signed_up, :with_piv_or_cac) } describe 'GET index' do it 'redirects to account page' do @@ -154,6 +154,15 @@ delete :delete expect(subject.user_session[:decrypted_x509]).to be_nil end + + it 'does not remove the piv/cac association if it is the last mfa method' do + user.phone_configurations.destroy_all + + delete :delete + + expect(response).to redirect_to(account_url) + expect(user.reload.x509_dn_uuid).to_not be_nil + end end end end diff --git a/spec/controllers/users/totp_setup_controller_spec.rb b/spec/controllers/users/totp_setup_controller_spec.rb index df773dd2d08..432d380defd 100644 --- a/spec/controllers/users/totp_setup_controller_spec.rb +++ b/spec/controllers/users/totp_setup_controller_spec.rb @@ -94,8 +94,8 @@ stub_sign_in(user) stub_analytics allow(@analytics).to receive(:track_event) + subject.user_session[:new_totp_secret] = 'abcdehij' - get :new patch :confirm, params: { code: 123 } end @@ -108,30 +108,24 @@ success: false, errors: {}, totp_secret_present: true, + multi_factor_auth_method: 'totp', } - expect(@analytics).to have_received(:track_event).with(Analytics::TOTP_SETUP, result) + + expect(@analytics).to have_received(:track_event). + with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) end end context 'when user presents correct code' do before do user = build(:user, personal_key: 'ABCD-DEFG-HIJK-LMNO') + secret = ROTP::Base32.random_base32 stub_sign_in(user) stub_analytics allow(@analytics).to receive(:track_event) + subject.user_session[:new_totp_secret] = secret - code = '123455' - totp_secret = 'abdef' - subject.user_session[:new_totp_secret] = totp_secret - form = instance_double(TotpSetupForm) - - allow(TotpSetupForm).to receive(:new). - with(subject.current_user, totp_secret, code).and_return(form) - response = FormResponse.new(success: true, errors: {}) - allow(form).to receive(:submit).and_return(response) - - get :new - patch :confirm, params: { code: code } + patch :confirm, params: { code: generate_totp_code(secret) } end it 'redirects to account_path with a success message' do @@ -142,8 +136,12 @@ result = { success: true, errors: {}, + totp_secret_present: true, + multi_factor_auth_method: 'totp', } - expect(@analytics).to have_received(:track_event).with(Analytics::TOTP_SETUP, result) + + expect(@analytics).to have_received(:track_event). + with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) end end end @@ -154,8 +152,8 @@ stub_sign_in_before_2fa stub_analytics allow(@analytics).to receive(:track_event) + subject.user_session[:new_totp_secret] = 'abcdehij' - get :new patch :confirm, params: { code: 123 } end @@ -168,29 +166,22 @@ success: false, errors: {}, totp_secret_present: true, + multi_factor_auth_method: 'totp', } - expect(@analytics).to have_received(:track_event).with(Analytics::TOTP_SETUP, result) + expect(@analytics).to have_received(:track_event). + with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) end end context 'when user presents correct code' do before do + secret = ROTP::Base32.random_base32 stub_sign_in_before_2fa stub_analytics allow(@analytics).to receive(:track_event) + subject.user_session[:new_totp_secret] = secret - code = '123455' - totp_secret = 'abdef' - subject.user_session[:new_totp_secret] = totp_secret - form = instance_double(TotpSetupForm) - - allow(TotpSetupForm).to receive(:new). - with(subject.current_user, totp_secret, code).and_return(form) - response = FormResponse.new(success: true, errors: {}) - allow(form).to receive(:submit).and_return(response) - - get :new - patch :confirm, params: { code: code } + patch :confirm, params: { code: generate_totp_code(secret) } end it 'redirects to personal key page with a success message' do @@ -201,8 +192,12 @@ result = { success: true, errors: {}, + totp_secret_present: true, + multi_factor_auth_method: 'totp', } - expect(@analytics).to have_received(:track_event).with(Analytics::TOTP_SETUP, result) + + expect(@analytics).to have_received(:track_event). + with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) end end @@ -224,8 +219,11 @@ success: false, errors: {}, totp_secret_present: false, + multi_factor_auth_method: 'totp', } - expect(@analytics).to have_received(:track_event).with(Analytics::TOTP_SETUP, result) + + expect(@analytics).to have_received(:track_event). + with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) end end end @@ -251,5 +249,16 @@ expect(subject).to have_received(:create_user_event).with(:authenticator_disabled) end end + + context 'when totp is the last mfa method' do + it 'does not disable totp' do + user = create(:user, :with_authentication_app) + sign_in user + + delete :disable + expect(user.reload.otp_secret_key).to_not be_nil + expect(response).to redirect_to(account_path) + end + end end end diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index 39b4cc72bdb..6e99a3a9110 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -88,9 +88,14 @@ end it 'tracks the submission' do - result = { success: true, errors: {}, mfa_method_counts: { auth_app: 1, phone: 1 } } + result = { + success: true, + errors: {}, + mfa_method_counts: { auth_app: 1, phone: 1 }, + multi_factor_auth_method: 'webauthn', + } expect(@analytics).to receive(:track_event). - with(Analytics::WEBAUTHN_SETUP_SUBMITTED, result) + with(Analytics::MULTI_FACTOR_AUTH_SETUP, result) patch :confirm, params: params end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 70a196645da..5a141b35deb 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -11,12 +11,25 @@ it 'destroys the current user and redirects to sign in page, with a helpful flash message' do sign_in_as_user + subject.session[:user_confirmation_token] = '1' expect { delete :destroy }.to change(User, :count).by(-1) expect(response).to redirect_to(root_url) expect(flash.now[:success]).to eq t('sign_up.cancel.success') end + it 'does not destroy the user if the user is not in setup mode and is after 2fa' do + sign_in_as_user + + expect { delete :destroy }.to change(User, :count).by(0) + end + + it 'does not destroy the user if the user is not in setup mode and is before 2fa' do + sign_in_before_2fa + + expect { delete :destroy }.to change(User, :count).by(0) + end + it 'finds the proper user and removes their record without `current_user`' do confirmation_token = '1' diff --git a/spec/features/saml/loa1/account_creation_spec.rb b/spec/features/saml/loa1/account_creation_spec.rb index e7c30477e6d..e4889c17f06 100644 --- a/spec/features/saml/loa1/account_creation_spec.rb +++ b/spec/features/saml/loa1/account_creation_spec.rb @@ -19,13 +19,30 @@ it 'redirects to the branded start page' do authn_request = auth_request.create(saml_settings) visit authn_request - sp_request_id = ServiceProviderRequest.last.uuid click_link t('sign_up.registrations.create_account') submit_form_with_valid_email click_confirmation_link_in_email('test@test.com') click_button t('links.cancel_account_creation') - expect(current_url).to eq sign_up_start_url(request_id: sp_request_id) + expect(current_url).to eq sign_up_cancel_url + + click_button t('forms.buttons.cancel') + expect(current_url).to eq sign_up_start_url(request_id: ServiceProviderRequest.last.uuid) + end + + it 'redirects to the password page after cancelling the cancellation' do + authn_request = auth_request.create(saml_settings) + visit authn_request + click_link t('sign_up.registrations.create_account') + submit_form_with_valid_email + click_confirmation_link_in_email('test@test.com') + previous_url = current_url + click_button t('links.cancel_account_creation') + + expect(current_url).to eq sign_up_cancel_url + + click_link t('links.go_back') + expect(current_url).to eq previous_url end end end diff --git a/spec/features/users/piv_cac_management_spec.rb b/spec/features/users/piv_cac_management_spec.rb index c92c702ef1d..f91ebd35b99 100644 --- a/spec/features/users/piv_cac_management_spec.rb +++ b/spec/features/users/piv_cac_management_spec.rb @@ -22,10 +22,7 @@ def find_form(page, attributes) before(:each) do user.identities << [identity_with_sp] - allow(Figaro.env).to receive(:piv_cac_agencies).and_return( - ['Test Government Agency'].to_json - ) - PivCacService.send(:reset_piv_cac_avaialable_agencies) + allow_any_instance_of(ServiceProvider).to receive(:piv_cac).and_return(true) end scenario 'allows association of a piv/cac with an account' do @@ -132,8 +129,7 @@ def find_form(page, attributes) before(:each) do user.identities << [identity_with_sp] - allow(Figaro.env).to receive(:piv_cac_agencies).and_return('[]') - PivCacService.send(:reset_piv_cac_avaialable_agencies) + allow_any_instance_of(ServiceProvider).to receive(:piv_cac).and_return(false) end scenario "doesn't advertise association of a piv/cac with an account" do @@ -190,4 +186,19 @@ def find_form(page, attributes) expect(user.x509_dn_uuid).to be_nil end end + + context 'with PIV/CAC as the only MFA method' do + let(:user) { create(:user, :with_piv_or_cac) } + + scenario 'disallows disassociation PIV/CAC' do + sign_in_and_2fa_user(user) + visit account_path + + form = find_form(page, action: disable_piv_cac_url) + expect(form).to be_nil + + user.reload + expect(user.x509_dn_uuid).to_not be_nil + end + end end diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb index c18316701b4..b5c82affa95 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -26,7 +26,7 @@ end context 'user cancels on the enter password screen', email: true do - it 'returns them to the home page' do + it 'sends them to the cancel page' do email = 'test@test.com' visit sign_up_email_path @@ -36,7 +36,7 @@ click_on t('links.cancel_account_creation') - expect(current_path).to eq root_path + expect(current_path).to eq sign_up_cancel_path end end @@ -64,39 +64,6 @@ end context 'with js', js: true do - context 'sp loa1' do - it 'allows the user to toggle the modal' do - begin_sign_up_with_sp_and_loa(loa3: false) - expect(page).not_to have_xpath("//div[@id='cancel-action-modal']") - - click_on t('links.cancel') - expect(page).to have_xpath("//div[@id='cancel-action-modal']") - - click_on t('sign_up.buttons.continue') - expect(page).not_to have_xpath("//div[@id='cancel-action-modal']") - end - - it 'allows the user to delete their account and returns them to the branded start page' do - user = begin_sign_up_with_sp_and_loa(loa3: false) - - click_on t('links.cancel') - click_on t('sign_up.buttons.cancel') - - expect(page).to have_current_path(sign_up_start_path(request_id: '123')) - expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound - end - end - - context 'sp loa3' do - it 'behaves like loa1 when user has not finished sign up' do - begin_sign_up_with_sp_and_loa(loa3: true) - - click_on t('links.cancel') - - expect(page).to have_xpath("//input[@value=\"#{t('sign_up.buttons.cancel')}\"]") - end - end - context 'user enters their email as their password', email: true do it 'treats it as a weak password' do email = 'test@test.com' @@ -167,7 +134,7 @@ end it 'does not allow a user to choose piv/cac as 2FA method during sign up' do - allow(PivCacService).to receive(:piv_cac_available_for_agency?).and_return(false) + allow(PivCacService).to receive(:piv_cac_available_for_sp?).and_return(false) begin_sign_up_with_sp_and_loa(loa3: false) expect(page).to have_current_path two_factor_options_path diff --git a/spec/features/users/totp_management_spec.rb b/spec/features/users/totp_management_spec.rb new file mode 100644 index 00000000000..141b968eed4 --- /dev/null +++ b/spec/features/users/totp_management_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +describe 'totp management' do + context 'when the user has totp enabled' do + let(:user) { create(:user, :signed_up, :with_authentication_app) } + + it 'allows the user to disable their totp app' do + sign_in_and_2fa_user(user) + + expect(page).to have_content(t('account.index.authentication_app')) + form = find_form(page, action: disable_totp_url) + expect(form).to_not be_nil + + form.click_button(t('forms.buttons.disable')) + + expect(page).to have_current_path(account_path) + expect(user.reload.otp_secret_key).to be_nil + end + end + + context 'when totp is the only mfa method' do + let(:user) { create(:user, :with_authentication_app) } + + it 'does not shot the user the option to disable their totp app' do + sign_in_and_2fa_user(user) + + expect(page).to have_content(t('account.index.authentication_app')) + form = find_form(page, action: disable_totp_url) + expect(form).to be_nil + end + end + + context 'when the user has totp disabled' do + let(:user) { create(:user, :signed_up) } + + it 'allows the user to setup a totp app' do + sign_in_and_2fa_user(user) + + click_link t('forms.buttons.enable'), href: authenticator_setup_url + + secret = find('#qr-code').text + fill_in 'code', with: generate_totp_code(secret) + click_button 'Submit' + + expect(user.reload.otp_secret_key).to_not be_nil + end + end + + # :reek:NestedIterators + def find_form(page, attributes) + page.all('form').detect do |form| + attributes.all? { |key, value| form[key] == value } + end + end +end diff --git a/spec/forms/totp_setup_form_spec.rb b/spec/forms/totp_setup_form_spec.rb index 7c9693f1c34..7d82658f786 100644 --- a/spec/forms/totp_setup_form_spec.rb +++ b/spec/forms/totp_setup_form_spec.rb @@ -10,9 +10,13 @@ it 'returns FormResponse with success: true' do form = TotpSetupForm.new(user, secret, code) result = instance_double(FormResponse) + extra = { + totp_secret_present: true, + multi_factor_auth_method: 'totp', + } expect(FormResponse).to receive(:new). - with(success: true, errors: {}, extra: { totp_secret_present: true }).and_return(result) + with(success: true, errors: {}, extra: extra).and_return(result) expect(Event).to receive(:create). with(user_id: user.id, event_type: :authenticator_enabled) expect(form.submit).to eq result @@ -24,9 +28,13 @@ it 'returns FormResponse with success: false' do form = TotpSetupForm.new(user, secret, 'kode') result = instance_double(FormResponse) + extra = { + totp_secret_present: true, + multi_factor_auth_method: 'totp', + } expect(FormResponse).to receive(:new). - with(success: false, errors: {}, extra: { totp_secret_present: true }).and_return(result) + with(success: false, errors: {}, extra: extra).and_return(result) expect(Event).to_not receive(:create) expect(form.submit).to eq result expect(user.reload.totp_enabled?).to eq false @@ -39,9 +47,13 @@ it 'returns FormResponse with success: false' do form = TotpSetupForm.new(user, nil, 'kode') result = instance_double(FormResponse) + extra = { + totp_secret_present: false, + multi_factor_auth_method: 'totp', + } expect(FormResponse).to receive(:new). - with(success: false, errors: {}, extra: { totp_secret_present: false }).and_return(result) + with(success: false, errors: {}, extra: extra).and_return(result) expect(Event).to_not receive(:create) expect(form.submit).to eq result expect(user.reload.totp_enabled?).to eq false diff --git a/spec/forms/user_piv_cac_setup_form_spec.rb b/spec/forms/user_piv_cac_setup_form_spec.rb index ec5be960b35..b8e7427d12c 100644 --- a/spec/forms/user_piv_cac_setup_form_spec.rb +++ b/spec/forms/user_piv_cac_setup_form_spec.rb @@ -25,9 +25,10 @@ it 'returns FormResponse with success: true' do result = instance_double(FormResponse) + extra = { multi_factor_auth_method: 'piv_cac' } expect(FormResponse).to receive(:new). - with(success: true, errors: {}).and_return(result) + with(success: true, errors: {}, extra: extra).and_return(result) expect(Event).to receive(:create). with(user_id: user.id, event_type: :piv_cac_enabled) expect(form.submit).to eq result @@ -41,9 +42,10 @@ it 'returns FormResponse with success: false' do result = instance_double(FormResponse) + extra = { multi_factor_auth_method: 'piv_cac' } expect(FormResponse).to receive(:new). - with(success: false, errors: {}).and_return(result) + with(success: false, errors: {}, extra: extra).and_return(result) expect(Event).to_not receive(:create) expect(form.submit).to eq result expect(TwoFactorAuthentication::PivCacPolicy.new(user.reload).enabled?).to eq true @@ -57,9 +59,10 @@ it 'returns FormResponse with success: false' do result = instance_double(FormResponse) + extra = { multi_factor_auth_method: 'piv_cac' } expect(FormResponse).to receive(:new). - with(success: false, errors: {}).and_return(result) + with(success: false, errors: {}, extra: extra).and_return(result) expect(Event).to_not receive(:create) expect(form.submit).to eq result expect(TwoFactorAuthentication::PivCacPolicy.new(user.reload).enabled?).to eq false @@ -73,9 +76,10 @@ allow(user).to receive(:save!).and_raise(PG::UniqueViolation) result = instance_double(FormResponse) + extra = { multi_factor_auth_method: 'piv_cac' } expect(FormResponse).to receive(:new). - with(success: false, errors: {}).and_return(result) + with(success: false, errors: {}, extra: extra).and_return(result) expect(Event).to_not receive(:create) expect(form.submit).to eq result expect(TwoFactorAuthentication::PivCacPolicy.new(user.reload).enabled?).to eq false @@ -93,9 +97,10 @@ it 'returns FormResponse with success: false' do result = instance_double(FormResponse) + extra = { multi_factor_auth_method: 'piv_cac' } expect(FormResponse).to receive(:new). - with(success: false, errors: {}).and_return(result) + with(success: false, errors: {}, extra: extra).and_return(result) expect(Event).to_not receive(:create) expect(form.submit).to eq result expect(TwoFactorAuthentication::PivCacPolicy.new(user.reload).enabled?).to eq false @@ -112,9 +117,10 @@ it 'returns FormResponse with success: false' do result = instance_double(FormResponse) + extra = { multi_factor_auth_method: 'piv_cac' } expect(FormResponse).to receive(:new). - with(success: false, errors: {}).and_return(result) + with(success: false, errors: {}, extra: extra).and_return(result) expect(Event).to_not receive(:create) expect(form.submit).to eq result expect(form.error_type).to eq 'token.invalid' @@ -126,9 +132,10 @@ it 'returns FormResponse with success: false' do result = instance_double(FormResponse) + extra = { multi_factor_auth_method: 'piv_cac' } expect(FormResponse).to receive(:new). - with(success: false, errors: {}).and_return(result) + with(success: false, errors: {}, extra: extra).and_return(result) expect(Event).to_not receive(:create) expect(form.submit).to eq result expect(TwoFactorAuthentication::PivCacPolicy.new(user.reload).enabled?).to eq false diff --git a/spec/forms/webauthn_setup_form_spec.rb b/spec/forms/webauthn_setup_form_spec.rb index f13a45d7256..fd73b2680d9 100644 --- a/spec/forms/webauthn_setup_form_spec.rb +++ b/spec/forms/webauthn_setup_form_spec.rb @@ -17,7 +17,10 @@ client_data_json: client_data_json, name: 'mykey', } - extra_attributes = { mfa_method_counts: { webauthn: 1 } } + extra_attributes = { + mfa_method_counts: { webauthn: 1 }, + multi_factor_auth_method: 'webauthn', + } expect(FormResponse).to receive(:new). with(success: true, errors: {}, extra: extra_attributes).and_return(result) @@ -33,7 +36,10 @@ client_data_json: client_data_json, name: 'mykey', } - extra_attributes = { mfa_method_counts: {} } + extra_attributes = { + mfa_method_counts: {}, + multi_factor_auth_method: 'webauthn', + } expect(FormResponse).to receive(:new). with(success: false, errors: {}, extra: extra_attributes).and_return(result) @@ -50,7 +56,10 @@ client_data_json: client_data_json, name: 'mykey', } - extra_attributes = { mfa_method_counts: {} } + extra_attributes = { + mfa_method_counts: {}, + multi_factor_auth_method: 'webauthn', + } expect(FormResponse).to receive(:new). with(success: false, extra: extra_attributes, errors: diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb index 196c20b6162..811cd55bd34 100644 --- a/spec/models/identity_spec.rb +++ b/spec/models/identity_spec.rb @@ -131,10 +131,10 @@ end describe '#piv_cac_available?' do - context 'when agency configured to support piv/cac' do + context 'when sp is configured to support piv/cac' do before(:each) do - allow(PivCacService).to receive(:piv_cac_available_for_agency?).with( - service_provider.agency, identity_with_sp.user.email_addresses.map(&:email) + allow(PivCacService).to receive(:piv_cac_available_for_sp?).with( + service_provider, identity_with_sp.user.email_addresses.map(&:email) ).and_return(true) end @@ -143,10 +143,10 @@ end end - context 'when agency is not configured to support piv/cac' do + context 'when sp is not configured to support piv/cac' do before(:each) do - allow(PivCacService).to receive(:piv_cac_available_for_agency?).with( - service_provider.agency, identity_with_sp.user.email_addresses.map(&:email) + allow(PivCacService).to receive(:piv_cac_available_for_sp?).with( + service_provider, identity_with_sp.user.email_addresses.map(&:email) ).and_return(false) end diff --git a/spec/models/service_provider_spec.rb b/spec/models/service_provider_spec.rb index e39f6e40f7e..c3ff760621e 100644 --- a/spec/models/service_provider_spec.rb +++ b/spec/models/service_provider_spec.rb @@ -68,16 +68,16 @@ end describe 'piv_cac_available?' do - context 'when the service provider is with an enabled agency' do + context 'when the service provider has piv_cac enabled' do it 'is truthy' do - allow(PivCacService).to receive(:piv_cac_available_for_agency?).and_return(true) + allow(PivCacService).to receive(:piv_cac_available_for_sp?).and_return(true) expect(service_provider.piv_cac_available?).to be_truthy end end - context 'when the service provider agency is not enabled' do + context 'when the service provider does not have piv_cac enabled' do it 'is falsey' do - allow(PivCacService).to receive(:piv_cac_available_for_agency?).and_return(false) + allow(PivCacService).to receive(:piv_cac_available_for_sp?).and_return(false) expect(service_provider.piv_cac_available?).to be_falsey end @@ -88,8 +88,8 @@ it 'calls with the user email' do expect(PivCacService).to receive( - :piv_cac_available_for_agency? - ).with(service_provider.agency, user.email_addresses.map(&:email)) + :piv_cac_available_for_sp? + ).with(service_provider, user.email_addresses.map(&:email)) service_provider.piv_cac_available?(user) end diff --git a/spec/policies/two_factor_authentication/piv_cac_policy_spec.rb b/spec/policies/two_factor_authentication/piv_cac_policy_spec.rb index 115c3760ac8..9d839c1d4b7 100644 --- a/spec/policies/two_factor_authentication/piv_cac_policy_spec.rb +++ b/spec/policies/two_factor_authentication/piv_cac_policy_spec.rb @@ -38,13 +38,10 @@ context 'allowing it' do before(:each) do - allow(Figaro.env).to receive(:piv_cac_agencies).and_return( - [service_provider.agency].to_json - ) - PivCacService.send(:reset_piv_cac_avaialable_agencies) + allow_any_instance_of(ServiceProvider).to receive(:piv_cac).and_return(true) end - it 'does allows piv/cac' do + it 'does allow piv/cac' do expect(subject.available?).to be_truthy end end diff --git a/spec/presenters/cancellation_presenter_spec.rb b/spec/presenters/cancellation_presenter_spec.rb new file mode 100644 index 00000000000..a6dfa893276 --- /dev/null +++ b/spec/presenters/cancellation_presenter_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +describe CancellationPresenter do + let(:good_url) { 'http://example.com/asdf/qwerty' } + let(:good_url_with_path) { 'http://example.com/asdf?qwerty=123' } + let(:bad_url) { 'http://evil.com/asdf/qwerty' } + + let(:view_context) { ActionView::Base.new } + + subject { described_class.new(view_context: view_context) } + + describe '#go_back_link' do + let(:sign_up_path) { '/two_factor_options' } + + before do + allow(view_context).to receive(:sign_up_path).and_return(sign_up_path) + request = instance_double(ActionDispatch::Request) + allow(request).to receive(:env).and_return('HTTP_REFERER' => referer_header) + allow(view_context).to receive(:request).and_return(request) + end + + context 'without a referer header' do + let(:referer_header) { nil } + + it 'returns the sign_up_path' do + expect(subject.go_back_path).to eq(sign_up_path) + end + end + + context 'with a referer header' do + let(:referer_header) { 'http://www.example.com/asdf/qwerty' } + + it 'returns the path' do + expect(subject.go_back_path).to eq('/asdf/qwerty') + end + end + + context 'with a referer header with query params' do + let(:referer_header) { 'http://www.example.com/asdf?qwerty=123' } + + it 'returns the path with the query params' do + expect(subject.go_back_path).to eq('/asdf?qwerty=123') + end + end + + context 'with a referer header for a different domain' do + let(:referer_header) { 'http://www.evil.com/asdf/qwerty' } + + it 'returns the sign_up_path' do + expect(subject.go_back_path).to eq(sign_up_path) + end + end + + context 'with a referer header with a javascript scheme' do + let(:referer_header) { 'javascript://do-some-evil-stuff' } + + it 'returns the sign_up_path' do + expect(subject.go_back_path).to eq(sign_up_path) + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index fecc5585a22..de5ca14acfe 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -76,4 +76,8 @@ example.run Capybara.use_default_driver end + + config.before(:each, type: :feature) do + allow_any_instance_of(Geocoder::Result::Test).to receive(:language=) + end end diff --git a/spec/requests/frontend_analytics_spec.rb b/spec/requests/frontend_analytics_spec.rb index ab1dbb7d66f..98e4ad805fc 100644 --- a/spec/requests/frontend_analytics_spec.rb +++ b/spec/requests/frontend_analytics_spec.rb @@ -59,14 +59,5 @@ expect(analytics).to_not have_received(:track_event). with(Analytics::FRONTEND_BROWSER_CAPABILITIES, any_args) end - - it 'supports the legacy API format' do - sign_in_user - - post analytics_path, params: { available: true } - - expect(analytics).to have_received(:track_event). - with(Analytics::FRONTEND_BROWSER_CAPABILITIES, hash_including(platform_authenticator: true)) - end end end diff --git a/spec/services/piv_cac_service_spec.rb b/spec/services/piv_cac_service_spec.rb index d40528635a2..8ea8675efb2 100644 --- a/spec/services/piv_cac_service_spec.rb +++ b/spec/services/piv_cac_service_spec.rb @@ -159,12 +159,12 @@ end end - describe '#piv_cac_available_for_agency?' do - let(:subject) { PivCacService.piv_cac_available_for_agency?('foo', ['foo@example.com']) } + describe '#piv_cac_available?' do + let(:subject) { PivCacService.piv_cac_available_for_sp?(sp, ['foo@example.com']) } context 'with an agency not encouraged to use piv/cac for anyone' do + let(:sp) { ServiceProvider.new(issuer: 'foo', piv_cac: false) } before(:each) do - allow(PivCacService).to receive(:available_for_agency?).and_return(false) allow(PivCacService).to receive(:available_for_email?).and_return(false) end @@ -172,8 +172,10 @@ end context 'with an agency encouraged to use piv/cac for everyone' do + let(:sp) { ServiceProvider.new(issuer: 'foo', piv_cac: true) } + let(:subject) { PivCacService.piv_cac_available_for_sp?(sp, ['foo@example.com']) } + before(:each) do - allow(PivCacService).to receive(:available_for_agency?).and_return(true) allow(PivCacService).to receive(:available_for_email?).and_return(false) end @@ -181,29 +183,11 @@ end context 'with an agency encouraged to use piv/cac for certain email domains' do - before(:each) do - allow(PivCacService).to receive(:available_for_agency?).and_return(false) - allow(PivCacService).to receive(:available_for_email?).and_return(true) - end - - it { expect(subject).to eq true } - end - end - - describe '#available_for_agency?' do - let(:subject) { PivCacService.send(:available_for_agency?, 'foo') } + let(:sp) { ServiceProvider.new(issuer: 'foo', piv_cac: false) } + let(:subject) { PivCacService.piv_cac_available_for_sp?(sp, ['foo@example.com']) } - context 'with the agency not configured to be available' do before(:each) do - allow(Figaro.env).to receive(:piv_cac_agencies).and_return('["bar"]') - end - - it { expect(subject).to be_falsey } - end - - context 'with the agency configured to be available' do - before(:each) do - allow(Figaro.env).to receive(:piv_cac_agencies).and_return('["bar","foo"]') + allow(PivCacService).to receive(:available_for_email?).and_return(true) end it { expect(subject).to eq true } @@ -211,7 +195,8 @@ end describe '#available_for_email?' do - let(:subject) { PivCacService.send(:available_for_email?, 'foo', ['foo@bar.example.com']) } + let(:sp) { ServiceProvider.new(issuer: 'foo', piv_cac: true, piv_cac_scoped_by_email: true) } + let(:subject) { PivCacService.send(:available_for_email?, sp, ['foo@bar.example.com']) } context 'with the agency not configured to be available' do before(:each) do diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 68c4c634ce0..ba51e1b8d30 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -415,7 +415,7 @@ def set_up_2fa_with_authenticator_app end def register_user_with_piv_cac(email = 'test@test.com') - allow(PivCacService).to receive(:piv_cac_available_for_agency?).and_return(true) + allow(PivCacService).to receive(:piv_cac_available_for_sp?).and_return(true) confirm_email_and_password(email) expect(page).to have_current_path two_factor_options_path diff --git a/spec/support/geocoder_stubs.rb b/spec/support/geocoder_stubs.rb new file mode 100644 index 00000000000..4a5a58728dc --- /dev/null +++ b/spec/support/geocoder_stubs.rb @@ -0,0 +1,41 @@ +Geocoder.configure(ip_lookup: :test) + +Geocoder::Lookup::Test.add_stub( + '1.2.3.4', [ + { + 'city' => 'foo', + 'country' => 'United States', + 'state_code' => '', + }, + ] +) + +Geocoder::Lookup::Test.add_stub( + '159.142.31.80', [ + { + 'city' => 'Arlington', + 'country' => 'United States', + 'state_code' => 'VA', + }, + ] +) + +Geocoder::Lookup::Test.add_stub( + '4.3.2.1', [ + { + 'city' => '', + 'country' => '', + 'state_code' => '', + }, + ] +) + +Geocoder::Lookup::Test.add_stub( + '127.0.0.1', [ + { + 'city' => '', + 'country' => 'United States', + 'state_code' => '', + }, + ] +) diff --git a/spec/view_models/account_show_spec.rb b/spec/view_models/account_show_spec.rb index 25c8dec550d..0c3195ac6a4 100644 --- a/spec/view_models/account_show_spec.rb +++ b/spec/view_models/account_show_spec.rb @@ -126,6 +126,9 @@ allow_any_instance_of( TwoFactorAuthentication::AuthAppPolicy ).to receive(:enabled?).and_return(true) + allow_any_instance_of( + MfaPolicy + ).to receive(:multiple_factors_enabled?).and_return(true) profile_index = AccountShow.new( decrypted_pii: {}, personal_key: '', decorated_user: user.decorate diff --git a/spec/views/accounts/show.html.slim_spec.rb b/spec/views/accounts/show.html.slim_spec.rb index be4c5c10cde..d9b13a118fe 100644 --- a/spec/views/accounts/show.html.slim_spec.rb +++ b/spec/views/accounts/show.html.slim_spec.rb @@ -11,6 +11,7 @@ :view_model, AccountShow.new(decrypted_pii: nil, personal_key: nil, decorated_user: decorated_user) ) + assign(:login_presenter, LoginPresenter.new(user: user)) end context 'user is not TOTP enabled' do @@ -190,4 +191,55 @@ end end end + + describe 'sign in timestamps and IP addresses' do + before do + current_sign_in_at = Time.zone.now - 5.seconds + last_sign_in_at = Time.zone.now - 5.seconds + user = build( + :user, + :signed_up, + :with_email, + current_sign_in_at: current_sign_in_at, + last_sign_in_at: last_sign_in_at, + current_sign_in_ip: '1.2.3.4', + last_sign_in_ip: '159.142.31.80' + ) + allow(view).to receive(:current_user).and_return(user) + assign(:login_presenter, LoginPresenter.new(user: user)) + allow_any_instance_of(Geocoder::Result::Test).to receive(:language=) + end + + it 'uses distance of time in words for timestamp' do + render + + expect(rendered).to have_content 'seconds ago' + end + + it 'only shows the country if city and state not geocoded from IP address' do + render + + expect(rendered).to have_content 'From United States (IP address: 1.2.3.4)' + end + + it 'shows city and state when geocoded from IP address' do + render + + expect(rendered).to have_content 'From Arlington, VA (IP address: 159.142.31.80)' + end + + it 'shows unknown location when IP address cannot be geocoded' do + user = build( + :user, + :signed_up, + :with_email, + current_sign_in_ip: '4.3.2.1' + ) + assign(:login_presenter, LoginPresenter.new(user: user)) + + render + + expect(rendered).to have_content 'From unknown location (IP address: 4.3.2.1)' + end + end end