diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 22687942cbe..00000000000 --- a/Dockerfile +++ /dev/null @@ -1,53 +0,0 @@ -# Use the official Ruby image because the Rails images have been deprecated -FROM ruby:2.5 - -# Enable https -RUN apt-get update -RUN apt-get install -y apt-transport-https - -# Install Postgres client -RUN apt-get install -y --no-install-recommends postgresql-client -RUN rm -rf /var/lib/apt/lists/* - -# Install Chrome for capybara -RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - -RUN sh -c 'echo "deb https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' -RUN apt-get update -RUN apt-get install -y google-chrome-stable - -# Install Node 12.x -RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - -RUN apt-get install -y nodejs -RUN ln -s ../node/bin/node /usr/local/bin/ -RUN ln -s ../node/bin/npm /usr/local/bin/ - -# Install Yarn -RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - -RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list -RUN apt-get update && apt-get install yarn - -# Everything happens here from now on -WORKDIR /upaya - -# Simple Gem cache. Success here creates a new layer in the image. -COPY Gemfile . -COPY Gemfile.lock . -RUN gem install bundler --conservative -RUN bundle install --without deploy production - -# Simple npm cache. Success here creates a new layer in the image. -COPY package.json . -COPY yarn.lock . -RUN yarn install --force - -# Copy everything else over -COPY . . - -# Up to this point we've been root, change to a lower priv. user -RUN groupadd -r appuser -RUN useradd --system --create-home --gid appuser appuser -RUN chown -R appuser.appuser /upaya -USER appuser - -EXPOSE 3000 -CMD ["rackup", "config.ru", "--host", "0.0.0.0", "--port", "3000"] diff --git a/Gemfile b/Gemfile index bde8af978de..36e0acdae1b 100644 --- a/Gemfile +++ b/Gemfile @@ -66,7 +66,7 @@ gem 'typhoeus' gem 'uglifier', '~> 3.2' gem 'user_agent_parser' gem 'valid_email' -gem 'webauthn', '~> 1.18.0' +gem 'webauthn', '~> 2.1.0' gem 'webpacker', '~> 3.4' gem 'xmlenc', '~> 0.6' gem 'zxcvbn-js' diff --git a/Gemfile.lock b/Gemfile.lock index c65bf35ce4d..e71a9fa3884 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,10 +43,10 @@ GIT GIT remote: https://github.com/18F/saml_idp.git - revision: f0c4a98952ca24e3c356293dd74fd4885d96ff37 + revision: 95366bbb24088660eadc54ba49f56399cf8b17b4 branch: master specs: - saml_idp (0.8.0.pre.18f) + saml_idp (0.9.0.pre.18f) activesupport builder httparty @@ -122,6 +122,7 @@ GEM american_date (1.1.1) arel (8.0.0) ast (2.4.0) + awrence (1.1.1) aws-eventstream (1.0.3) aws-partitions (1.235.0) aws-sdk-core (3.75.0) @@ -166,7 +167,7 @@ GEM debug_inspector (>= 0.0.1) brakeman (4.7.1) browser (2.6.1) - builder (3.2.3) + builder (3.2.4) bullet (6.0.2) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) @@ -198,9 +199,9 @@ GEM coercible (1.0.0) descendants_tracker (~> 0.0.1) colorize (0.8.1) - concurrent-ruby (1.1.5) + concurrent-ruby (1.1.6) connection_pool (2.2.2) - cose (0.7.0) + cose (0.10.0) cbor (~> 0.5.9) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -299,10 +300,10 @@ GEM hiredis (0.6.3) htmlentities (4.3.4) http_accept_language (2.1.1) - httparty (0.17.1) + httparty (0.18.0) mime-types (~> 3.0) multi_xml (>= 0.5.2) - i18n (1.7.0) + i18n (1.7.1) concurrent-ruby (~> 1.0) i18n-tasks (0.9.29) activesupport (>= 4.0.2) @@ -349,12 +350,12 @@ GEM maxminddb (0.1.22) memory_profiler (0.9.14) method_source (0.9.2) - mime-types (3.3) + mime-types (3.3.1) mime-types-data (~> 3.2015) mime-types-data (3.2019.1009) mini_mime (1.0.2) mini_portile2 (2.4.0) - minitest (5.13.0) + minitest (5.14.0) multi_xml (0.6.0) multipart-post (2.1.1) mustermann (1.0.3) @@ -364,7 +365,7 @@ GEM net-ssh (5.2.0) newrelic_rpm (6.7.0.359) nio4r (2.5.2) - nokogiri (1.10.5) + nokogiri (1.10.8) mini_portile2 (~> 2.4.0) notiffany (0.1.3) nenv (~> 0.1) @@ -383,7 +384,7 @@ GEM ast (~> 2.4.0) pg (1.1.4) phonelib (0.6.39) - pkcs11 (0.2.7) + pkcs11 (0.3.2) premailer (1.11.1) addressable css_parser (>= 1.6.0) @@ -404,7 +405,7 @@ GEM pry (>= 0.10.4) psych (3.1.0) public_suffix (4.0.1) - puma (4.3.1) + puma (4.3.3) nio4r (~> 2.0) rack (2.0.8) rack-attack (6.2.1) @@ -609,7 +610,7 @@ GEM rotp (>= 3.2.0) typhoeus (1.3.1) ethon (>= 0.9.0) - tzinfo (1.2.5) + tzinfo (1.2.6) thread_safe (~> 0.1) uglifier (3.2.0) execjs (>= 0.3.0, < 3) @@ -628,10 +629,11 @@ GEM equalizer (~> 0.0, >= 0.0.9) warden (1.2.8) rack (>= 2.0.6) - webauthn (1.18.0) + webauthn (2.1.0) + awrence (~> 1.1) bindata (~> 2.4) cbor (~> 0.5.9) - cose (~> 0.7.0) + cose (~> 0.10.0) jwt (>= 1.5, < 3.0) openssl (~> 2.0) securecompare (~> 1.0) @@ -771,7 +773,7 @@ DEPENDENCIES uglifier (~> 3.2) user_agent_parser valid_email - webauthn (~> 1.18.0) + webauthn (~> 2.1.0) webdrivers (~> 3.0) webmock webpacker (~> 3.4) diff --git a/README.md b/README.md index 98a89d585af..31ab8ea26d9 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,11 @@ A Identity Management System powering login.gov. - [Node.js v12.x.x](https://nodejs.org) - [Yarn](https://yarnpkg.com/en/) -#### Setting up and running the app +#### Running the app with Docker + +See the [Docker documentation](./docs/Docker.md) to get up and running + +#### Setting up and running the app without Docker 1. Make sure you have a working development environment with all the [dependencies](#dependencies) installed. On OS X, the easiest way @@ -180,37 +184,6 @@ it into the "Index pattern" field, then click the "Next step" button. 12. Refresh the Kibana website. You should now see new events show up in the Discover section. - -#### Using Docker Locally - -1. Download, install, and launch [Docker](https://www.docker.com/products/docker-desktop). You should probably bump the memory resources in Docker above the defaults to avoid timeouts. 4 or 8 GB should work well. - -1. Build the Docker containers: `docker-compose build` - -1. Run `make docker_setup` to copy configuration files and bootstrap the database. - -1. Start the Docker containers `docker-compose up` and `open http://localhost:3000` - -Please note that the `docker_setup` script will destroy and re-create configuration files that were previously symlinked. See the script source for more info. - -More useful Docker commands: - -* Force the images to re-build: `docker-compose build --no-cache` -* Stop the containers: `docker-compose stop` -* Stop and remove the containers (`-v` removes Volumes, which includes Postgres data): `docker-compose down` -* Open a shell in a one-off web container: `docker-compose run --rm web bash` -* Open a shell in the running web container: `docker-compose exec web bash` -* Open a psql shell in the running db container: `docker-compose exec db psql -U postgres` - -#### Running Tests in Docker - -* After Docker is set up you can run the entire suite with `docker-compose run web bundle exec rspec`. This takes a while. -* You can run a one-off test with `docker-compose run web bundle exec rspec spec/file.rb` -* If the cluster is already running you can run the test on those containers using `exec` instead of `run`: `docker-compose exec web bundle exec rspec spec/file.rb` - - - - ### Viewing the app locally Once it is up and running, the app will be accessible at diff --git a/app/assets/stylesheets/components/_list.scss b/app/assets/stylesheets/components/_list.scss index 52e95ea495f..613928658ad 100644 --- a/app/assets/stylesheets/components/_list.scss +++ b/app/assets/stylesheets/components/_list.scss @@ -34,7 +34,9 @@ background-repeat: no-repeat; content: ''; display: inline-block; + float: left; height: 1rem; + margin-top: .33rem; padding-right: 1.5rem; vertical-align: middle; width: 1rem; diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index 0d9f2e22e4d..1b3456116cd 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -1,3 +1,4 @@ +# rubocop:disable Metrics/ModuleLength module SamlIdpAuthConcern extend ActiveSupport::Concern @@ -94,6 +95,7 @@ def saml_response reference_id: active_identity.session_uuid, encryption: current_service_provider.encryption_opts, signature: saml_response_signature_options, + signed_response_message: current_service_provider.signed_response_message_requested, ) end @@ -126,3 +128,4 @@ def request_url url.to_s end end +# rubocop:enable Metrics/ModuleLength diff --git a/app/controllers/concerns/verify_profile_concern.rb b/app/controllers/concerns/verify_profile_concern.rb index 972e9c85206..bbf598817d6 100644 --- a/app/controllers/concerns/verify_profile_concern.rb +++ b/app/controllers/concerns/verify_profile_concern.rb @@ -10,6 +10,7 @@ def account_or_verify_profile_url end def account_or_verify_profile_route + return 'idv' if session[:ial2_request_with_no_sp] && current_user.active_profile.blank? return 'account' unless profile_needs_verification? return 'idv_usps' if usps_mail_bounced? 'verify_account' diff --git a/app/controllers/concerns/verify_sp_attributes_concern.rb b/app/controllers/concerns/verify_sp_attributes_concern.rb index 85bc0df66cf..bc924f42b78 100644 --- a/app/controllers/concerns/verify_sp_attributes_concern.rb +++ b/app/controllers/concerns/verify_sp_attributes_concern.rb @@ -1,6 +1,9 @@ module VerifySPAttributesConcern def needs_completions_screen? - sp_session[:issuer].present? && (sp_session_identity.nil? || !requested_attributes_verified?) + sp_session[:issuer].present? && + (sp_session_identity.nil? || + !requested_attributes_verified? || + consent_has_expired?) end def needs_sp_attribute_verification? @@ -20,6 +23,7 @@ def update_verified_attributes ).link_identity( ial: sp_session_ial, verified_attributes: sp_session[:requested_attributes], + last_consented_at: Time.zone.now, ) end @@ -36,6 +40,12 @@ def clear_verify_attributes_sessions user_session[:verify_shared_attributes] = false end + def consent_has_expired? + return false unless sp_session_identity + last_estimated_consent = sp_session_identity.last_consented_at || sp_session_identity.created_at + !last_estimated_consent || last_estimated_consent < Identity::CONSENT_EXPIRATION.ago + end + private def sp_session_identity diff --git a/app/controllers/idv_controller.rb b/app/controllers/idv_controller.rb index 856931eda4b..d3714172bb6 100644 --- a/app/controllers/idv_controller.rb +++ b/app/controllers/idv_controller.rb @@ -18,6 +18,7 @@ def index def activated redirect_to idv_url unless active_profile? + redirect_to account_url if session[:ial2_request_with_no_sp] idv_session.clear end diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index 6be0c8e1d3f..ec95826f7f0 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -44,6 +44,7 @@ def view_model current_user: current_user, handoff: new_service_provider_attributes, ialmax_requested: ialmax?, + consent_has_expired: consent_has_expired?, ) end @@ -115,6 +116,7 @@ def displayable_attributes return pii_to_displayable_attributes if user_session['decrypted_pii'].present? { email: email, + verified_at: verified_at, x509_subject: current_user.piv_cac_configurations.first&.x509_dn_uuid, } end @@ -124,6 +126,15 @@ def dob pii_dob ? pii_dob.to_date.to_formatted_s(:long) : '' end + def verified_at + timestamp = current_user.active_profile&.verified_at + if timestamp + I18n.l(timestamp, format: :event_timestamp) + else + I18n.t('help_text.requested_attributes.verified_at_blank') + end + end + def pii_to_displayable_attributes { full_name: full_name, @@ -132,6 +143,7 @@ def pii_to_displayable_attributes birthdate: dob, phone: PhoneFormatter.format(pii[:phone].to_s), email: email, + verified_at: verified_at, x509_subject: current_user.piv_cac_configurations.first&.x509_dn_uuid, } end diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index d11a613bb9d..ad60daef475 100644 --- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb +++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb @@ -66,8 +66,8 @@ def user_opted_remember_device_cookie end def save_challenge_in_session - credential_creation_options = ::WebAuthn.credential_request_options - user_session[:webauthn_challenge] = credential_creation_options[:challenge].bytes.to_a + credential_creation_options = WebAuthn::Credential.options_for_get + user_session[:webauthn_challenge] = credential_creation_options.challenge.bytes.to_a end def credential_ids diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 9ca9fe32a36..f3008ee58f1 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -21,6 +21,7 @@ def new ) @ial = sp_session ? sp_session_ial : 1 + session[:ial2_request_with_no_sp] = true if sp_session.blank? && params[:ial] == '2' super end diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index 372274fcaff..0f0425916a5 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -11,7 +11,7 @@ def show def send_code result = otp_delivery_selection_form.submit(delivery_params) - add_tracking(result) + analytics.track_event(Analytics::OTP_DELIVERY_SELECTION, result.to_h) if result.success? handle_valid_otp_params(user_select_delivery_preference, user_selected_default_number) update_otp_delivery_preference_if_needed @@ -22,11 +22,6 @@ def send_code private - def add_tracking(result) - analytics.track_event(Analytics::OTP_DELIVERY_SELECTION, result.to_h) - add_sp_cost(delivery_preference) - end - def phone_enabled? phone_configuration&.mfa_enabled? end @@ -135,7 +130,7 @@ def handle_valid_otp_params(method, default = nil) end def handle_telephony_result(method:, default:) - analytics.track_event(Analytics::TELEPHONY_OTP_SENT, @telephony_result.to_h) + track_events(method) if @telephony_result.success? redirect_to login_two_factor_url( otp_delivery_preference: method, @@ -147,6 +142,11 @@ def handle_telephony_result(method:, default:) end end + def track_events(method) + analytics.track_event(Analytics::TELEPHONY_OTP_SENT, @telephony_result.to_h) + add_sp_cost(method) if @telephony_result.success? + end + def exceeded_otp_send_limit? return otp_rate_limiter.lock_out_user if otp_rate_limiter.exceeded_otp_send_limit? end diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index 29f6d8651e7..397146183e2 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -85,8 +85,8 @@ def track_delete(success) end def save_challenge_in_session - credential_creation_options = ::WebAuthn.credential_creation_options - user_session[:webauthn_challenge] = credential_creation_options[:challenge].bytes.to_a + credential_creation_options = WebAuthn::Credential.options_for_create(user: current_user) + user_session[:webauthn_challenge] = credential_creation_options.challenge.bytes.to_a end def process_valid_webauthn diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index bac0d9edca0..26db152926d 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -53,6 +53,10 @@ def submit FormResponse.new(success: success, errors: errors.messages, extra: extra_analytics_attributes) end + def ial2_service_provider? + service_provider.ial == 2 + end + def ial2_requested? ial == 2 end @@ -61,6 +65,10 @@ def ialmax_requested? ial&.zero? end + def verified_at_requested? + scope.include?('profile:verified_at') + end + def service_provider @_service_provider ||= ServiceProvider.from_issuer(client_id) end @@ -90,6 +98,7 @@ def success_redirect_uri def check_for_unauthorized_scope(params) param_value = params[:scope] return false if ial2_requested? || param_value.blank? + return true if verified_at_requested? && !ial2_service_provider? @scope != param_value.split(' ').compact end @@ -166,8 +175,8 @@ def scopes end def validate_privileges - if (ial2_requested? && service_provider.ial != 2) || - (ialmax_requested? && service_provider.ial != 2) + if (ial2_requested? && !ial2_service_provider?) || + (ialmax_requested? && !ial2_service_provider?) errors.add(:acr_values, t('openid_connect.authorization.errors.no_auth')) end end diff --git a/app/forms/webauthn_verification_form.rb b/app/forms/webauthn_verification_form.rb index a8560aeeef3..603e41ce93a 100644 --- a/app/forms/webauthn_verification_form.rb +++ b/app/forms/webauthn_verification_form.rb @@ -53,18 +53,10 @@ def valid_assertion_response?(protocol) authenticator_data: Base64.decode64(@authenticator_data), client_data_json: Base64.decode64(@client_data_json), signature: Base64.decode64(@signature), - credential_id: Base64.decode64(@credential_id), ) original_origin = "#{protocol}#{self.class.domain_name}" assertion_response.valid?(@challenge.pack('c*'), original_origin, - allowed_credentials: [allowed_credential]) - end - - def allowed_credential - { - id: Base64.decode64(@credential_id), - public_key: Base64.decode64(public_key), - } + public_key: Base64.decode64(public_key), sign_count: 0) end def public_key diff --git a/app/javascript/app/webauthn.js b/app/javascript/app/webauthn.js index b042cdc8211..9858187e7fe 100644 --- a/app/javascript/app/webauthn.js +++ b/app/javascript/app/webauthn.js @@ -15,10 +15,15 @@ const longToByteArray = long => new Uint8Array(8).map(() => { return byte; }); -const extractCredentials = credentials => credentials.split(',').map(credential => ({ - type: 'public-key', - id: base64ToArrayBuffer(credential), -})); +const extractCredentials = (credentials) => { + if (!credentials) { // empty string check + return []; + } + return credentials.split(',').map(credential => ({ + type: 'public-key', + id: base64ToArrayBuffer(credential), + })); +}; const isWebAuthnEnabled = () => { if (navigator && navigator.credentials && navigator.credentials.create) { @@ -70,6 +75,12 @@ const enrollWebauthnDevice = ({ userId, userEmail, userChallenge, excludeCredent timeout: 800000, attestation: 'none', excludeList: [], + authenticatorSelection: { + // Prevents user from needing to use PIN with Security Key + userVerification: 'discouraged', + // Defaults to "Security Key" instead of things like "Windows Hello" + authenticatorAttachment: 'cross-platform', + }, excludeCredentials: extractCredentials(excludeCredentials), }, }; @@ -101,6 +112,7 @@ const verifyWebauthnDevice = ({ userChallenge, credentialIds }) => { }; export { + extractCredentials, isWebAuthnEnabled, enrollWebauthnDevice, verifyWebauthnDevice, diff --git a/app/models/identity.rb b/app/models/identity.rb index 6d300df1add..b5c6517d21c 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -4,12 +4,16 @@ class Identity < ApplicationRecord belongs_to :user validates :service_provider, presence: true + delegate :metadata, to: :sp, prefix: true + + CONSENT_EXPIRATION = 1.year + def deactivate update!(session_uuid: nil) end - def sp_metadata - ServiceProvider.from_issuer(service_provider).metadata + def sp + @sp ||= ServiceProvider.from_issuer(service_provider) end def display_name diff --git a/app/presenters/openid_connect_user_info_presenter.rb b/app/presenters/openid_connect_user_info_presenter.rb index ed8c2b08d9b..3d0ceb4394b 100644 --- a/app/presenters/openid_connect_user_info_presenter.rb +++ b/app/presenters/openid_connect_user_info_presenter.rb @@ -13,6 +13,7 @@ def user_info iss: root_url, email: email_from_sp_identity(identity), email_verified: true, + verified_at: verified_at, }. merge(x509_attributes). merge(ial2_attributes) @@ -111,4 +112,10 @@ def x509_data def x509_session? identity.piv_cac_enabled? end + + def verified_at + return if identity.sp.ial.to_i < 2 + + identity.user.active_profile&.verified_at&.to_i + end end diff --git a/app/presenters/saml_request_presenter.rb b/app/presenters/saml_request_presenter.rb index f9154cb5d21..8903c792748 100644 --- a/app/presenters/saml_request_presenter.rb +++ b/app/presenters/saml_request_presenter.rb @@ -11,6 +11,7 @@ class SamlRequestPresenter address2: :address, city: :address, state: :address, + verified_at: :verified_at, zipcode: :address, }.freeze @@ -20,8 +21,13 @@ def initialize(request:, service_provider:) end def requested_attributes - return [:email] unless ial2_authn_context? || ialmax_authn_context? - bundle.map { |attr| ATTRIBUTE_TO_FRIENDLY_NAME_MAP[attr] }.compact.uniq + if ial2_authn_context? || ialmax_authn_context? + bundle.map { |attr| ATTRIBUTE_TO_FRIENDLY_NAME_MAP[attr] }.compact.uniq + else + attrs = [:email] + attrs << :verified_at if bundle.include?(:verified_at) + attrs + end end private diff --git a/app/services/attribute_asserter.rb b/app/services/attribute_asserter.rb index 7561e57eee4..e67330812c0 100644 --- a/app/services/attribute_asserter.rb +++ b/app/services/attribute_asserter.rb @@ -27,6 +27,7 @@ def build attrs = default_attrs add_email(attrs) if bundle.include? :email add_bundle(attrs) if user.active_profile.present? && ial2_authn_context? + add_verified_at(attrs) if bundle.include?(:verified_at) && ial2_service_provider? user.asserted_attributes = attrs end @@ -50,6 +51,10 @@ def add_bundle(attrs) getter = ascii? ? attribute_getter_function_ascii(attr) : attribute_getter_function(attr) attrs[attr] = { getter: getter } end + add_verified_at(attrs) + end + + def add_verified_at(attrs) attrs[:verified_at] = { getter: verified_at_getter_function } end @@ -61,7 +66,7 @@ def uuid_getter_function end def verified_at_getter_function - ->(principal) { principal.active_profile.verified_at.iso8601 } + ->(principal) { principal.active_profile&.verified_at&.iso8601 } end def attribute_getter_function(attr) @@ -101,4 +106,8 @@ def authn_context def ascii? bundle.include?(:ascii) end + + def ial2_service_provider? + service_provider.ial.to_i >= 2 + end end diff --git a/app/services/db/identity/sp_active_user_counts.rb b/app/services/db/identity/sp_active_user_counts.rb new file mode 100644 index 00000000000..feb8ab2cd31 --- /dev/null +++ b/app/services/db/identity/sp_active_user_counts.rb @@ -0,0 +1,36 @@ +module Db + module Identity + class SpActiveUserCounts + # rubocop:disable Metrics/MethodLength + def self.call(start_date) + quoted_start_date = ActiveRecord::Base.connection.quote(start_date) + sql = <<~SQL + SELECT + issuer, + CAST(SUM(total_ial1_active) AS INTEGER) AS total_ial1_active, + CAST(SUM(total_ial2_active) AS INTEGER) AS total_ial2_active + FROM ( + (SELECT + service_provider AS issuer, + count(*) AS total_ial1_active, + 0 AS total_ial2_active + FROM identities + WHERE #{quoted_start_date} <= last_ial1_authenticated_at + GROUP BY issuer ORDER BY issuer) + UNION + (SELECT + service_provider AS issuer, + 0 AS total_ial1_active, + count(*) AS total_ial2_active + FROM identities + WHERE #{quoted_start_date} <= last_ial2_authenticated_at + GROUP BY issuer ORDER BY issuer) + ) AS union_of_ial1_and_ial2_results + GROUP BY ISSUER ORDER BY issuer + SQL + ActiveRecord::Base.connection.execute(sql) + end + # rubocop:enable Metrics/MethodLength + end + end +end diff --git a/app/services/db/identity/sp_user_counts.rb b/app/services/db/identity/sp_user_counts.rb index 64aa7fa41ac..bba2e0bd1cc 100644 --- a/app/services/db/identity/sp_user_counts.rb +++ b/app/services/db/identity/sp_user_counts.rb @@ -1,14 +1,20 @@ module Db module Identity class SpUserCounts + # rubocop:disable Metrics/MethodLength def self.call sql = <<~SQL - SELECT service_provider as issuer,count(user_id) AS total + SELECT + service_provider AS issuer, + count(user_id) AS total, + count(user_id)-count(verified_at) AS ial1_total, + count(verified_at) AS ial2_total FROM identities GROUP BY issuer ORDER BY issuer SQL ActiveRecord::Base.connection.execute(sql) end + # rubocop:enable Metrics/MethodLength end end end diff --git a/app/services/db/identity/sp_user_quotas.rb b/app/services/db/identity/sp_user_quotas.rb new file mode 100644 index 00000000000..c1e7c92264e --- /dev/null +++ b/app/services/db/identity/sp_user_quotas.rb @@ -0,0 +1,32 @@ +module Db + module Identity + class SpUserQuotas + # rubocop:disable Metrics/MethodLength + def self.call(start_date) + sql = <<~SQL + SELECT + service_providers.issuer, + ial2_total, + CAST( + (CASE WHEN ial2_quota IS NULL + THEN 0 + ELSE ROUND(ial2_total * 100.0 / ial2_quota) + END) + AS INTEGER + ) AS percent_ial2_quota + FROM service_providers, + (SELECT + service_provider AS issuer, + count(verified_at) AS ial2_total + FROM identities + WHERE '#{start_date}' <= verified_at + GROUP BY issuer ORDER BY issuer) + AS TBL + WHERE TBL.issuer = service_providers.issuer + SQL + ActiveRecord::Base.connection.execute(sql) + end + # rubocop:enable Metrics/MethodLength + end + end +end diff --git a/app/services/identity_linker.rb b/app/services/identity_linker.rb index d870656750d..f116ccc1392 100644 --- a/app/services/identity_linker.rb +++ b/app/services/identity_linker.rb @@ -8,7 +8,7 @@ def initialize(user, provider) end def link_identity(**extra_attrs) - @ial = extra_attrs[:ial] + process_ial(extra_attrs) attributes = merged_attributes(extra_attrs) identity.update!(attributes) AgencyIdentityLinker.new(identity).link_identity @@ -21,6 +21,26 @@ def already_linked? private + def process_ial(extra_attrs) + @ial = extra_attrs[:ial] + now = Time.zone.now + process_ial_at(now) + process_verified_at(now) + end + + def process_ial_at(now) + if @ial == 2 || (identity.verified_at.present? && @ial&.zero?) + identity.last_ial2_authenticated_at = now + else + identity.last_ial1_authenticated_at = now + end + end + + def process_verified_at(now) + return unless @ial == 2 && identity.verified_at.nil? + identity.verified_at = now + end + def identity @identity ||= find_or_create_identity_with_costing end @@ -54,7 +74,8 @@ def optional_attributes( nonce: nil, rails_session_id: nil, scope: nil, - verified_attributes: nil + verified_attributes: nil, + last_consented_at: nil ) { code_challenge: code_challenge, @@ -63,7 +84,9 @@ def optional_attributes( rails_session_id: rails_session_id, scope: scope, verified_attributes: merge_attributes(verified_attributes), - } + }.tap do |hash| + hash[:last_consented_at] = last_consented_at if last_consented_at + end end def merge_attributes(verified_attributes) diff --git a/app/services/openid_connect_attribute_scoper.rb b/app/services/openid_connect_attribute_scoper.rb index 4e8336e927f..4779f9396f3 100644 --- a/app/services/openid_connect_attribute_scoper.rb +++ b/app/services/openid_connect_attribute_scoper.rb @@ -7,6 +7,7 @@ class OpenidConnectAttributeScoper profile profile:birthdate profile:name + profile:verified_at social_security_number x509 x509:subject @@ -16,6 +17,7 @@ class OpenidConnectAttributeScoper VALID_IAL1_SCOPES = %w[ email openid + profile:verified_at x509 x509:subject x509:presented @@ -30,6 +32,7 @@ class OpenidConnectAttributeScoper given_name: %w[profile profile:name], family_name: %w[profile profile:name], birthdate: %w[profile profile:birthdate], + verified_at: %w[profile profile:verified_at], social_security_number: %w[social_security_number], x509_subject: %w[x509 x509:subject], x509_presented: %w[x509 x509:presented], diff --git a/app/services/reports/base_report.rb b/app/services/reports/base_report.rb index ca5272648b2..92f32839c90 100644 --- a/app/services/reports/base_report.rb +++ b/app/services/reports/base_report.rb @@ -4,8 +4,13 @@ module Reports class BaseReport private + def fiscal_start_date + now = Time.zone.now + now.change(year: now.month >= 10 ? now.year : now.year - 1, month: 10, day: 1).to_date.to_s + end + def first_of_this_month - Time.zone.today.strftime('%m-01-%Y') + Time.zone.today.beginning_of_month.strftime('%m-%d-%Y') end def end_of_today diff --git a/app/services/reports/sp_active_users_report.rb b/app/services/reports/sp_active_users_report.rb new file mode 100644 index 00000000000..c4660809614 --- /dev/null +++ b/app/services/reports/sp_active_users_report.rb @@ -0,0 +1,14 @@ +require 'login_gov/hostdata' + +module Reports + class SpActiveUsersReport < BaseReport + REPORT_NAME = 'sp-active-users-report'.freeze + + def call + results = transaction_with_timeout do + Db::Identity::SpActiveUserCounts.call(fiscal_start_date) + end + save_report(REPORT_NAME, results.to_json) + end + end +end diff --git a/app/services/reports/sp_user_quotas_report.rb b/app/services/reports/sp_user_quotas_report.rb new file mode 100644 index 00000000000..c8cb011635f --- /dev/null +++ b/app/services/reports/sp_user_quotas_report.rb @@ -0,0 +1,14 @@ +require 'login_gov/hostdata' + +module Reports + class SpUserQuotasReport < BaseReport + REPORT_NAME = 'sp-user-quotas-report'.freeze + + def call + user_counts = transaction_with_timeout do + Db::Identity::SpUserQuotas.call(fiscal_start_date) + end + save_report(REPORT_NAME, user_counts.to_json) + end + end +end diff --git a/app/view_models/sign_up_completions_show.rb b/app/view_models/sign_up_completions_show.rb index dbe633681fd..3901a61f8fc 100644 --- a/app/view_models/sign_up_completions_show.rb +++ b/app/view_models/sign_up_completions_show.rb @@ -1,12 +1,15 @@ +# rubocop:disable Metrics/ClassLength class SignUpCompletionsShow include ActionView::Helpers::TagHelper - def initialize(ial2_requested:, decorated_session:, current_user:, handoff:, ialmax_requested:) + def initialize(ial2_requested:, decorated_session:, current_user:, handoff:, ialmax_requested:, + consent_has_expired:) @ial2_requested = ial2_requested @decorated_session = decorated_session @current_user = current_user @handoff = handoff @ialmax_requested = ialmax_requested + @consent_has_expired = consent_has_expired end attr_reader :ial2_requested, :ialmax_requested, :decorated_session @@ -19,18 +22,21 @@ def initialize(ial2_requested:, decorated_session:, current_user:, handoff:, ial [[:birthdate], :birthdate], [[:social_security_number], :social_security_number], [[:x509_subject], :x509_subject], + [[:verified_at], :verified_at], ].freeze SORTED_IAL1_ATTRIBUTE_MAPPING = [ [[:email], :email], [[:x509_subject], :x509_subject], + [[:verified_at], :verified_at], ].freeze MAX_RECENT_IDENTITIES = 5 # rubocop:disable Rails/OutputSafety def heading - return content_tag(:strong, I18n.t('titles.sign_up.new_sp')) if handoff? + return handoff_heading if handoff? + if requested_ial == 'ial2' return content_tag(:strong, I18n.t('titles.sign_up.verified', app: APP_NAME)) end @@ -106,10 +112,22 @@ def user_has_identities? private + def handoff_heading + if consent_has_expired? + content_tag(:strong, I18n.t('titles.sign_up.refresh_consent')) + else + content_tag(:strong, I18n.t('titles.sign_up.new_sp')) + end + end + def handoff? @handoff end + def consent_has_expired? + @consent_has_expired + end + def requested_attributes decorated_session.requested_attributes.map(&:to_sym) end @@ -122,3 +140,4 @@ def requested_ial user_verified? ? 'ial2' : 'ial1' end end +# rubocop:enable Metrics/ClassLength diff --git a/app/views/sign_up/completions/_requested_attributes.html.erb b/app/views/sign_up/completions/_requested_attributes.html.erb new file mode 100644 index 00000000000..9e92a9735d5 --- /dev/null +++ b/app/views/sign_up/completions/_requested_attributes.html.erb @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/app/views/sign_up/completions/_requested_attributes.html.slim b/app/views/sign_up/completions/_requested_attributes.html.slim deleted file mode 100644 index 012c4d019fd..00000000000 --- a/app/views/sign_up/completions/_requested_attributes.html.slim +++ /dev/null @@ -1,7 +0,0 @@ -ul.mb3.list-reset.border-bottom.success-bullets.requested-attributes - - view_model.requested_attributes_sorted.each do |attribute| - - next if @pii[attribute].blank? - li.border-top - span.bold - = t("help_text.requested_attributes.#{attribute}") - span.pl2 == @pii[attribute].to_s diff --git a/app/views/sign_up/completions/_show_sp.html.slim b/app/views/sign_up/completions/_show_sp.html.slim index 353d4748bf8..2341083e089 100644 --- a/app/views/sign_up/completions/_show_sp.html.slim +++ b/app/views/sign_up/completions/_show_sp.html.slim @@ -10,5 +10,5 @@ p.sm-mr1.sm-ml1.mt3.mb3 sp: content_tag(:strong, decorated_session.sp_agency)) p .center - = button_to t('forms.buttons.continue'), sign_up_completed_path, \ + = button_to t('sign_up.agree_and_continue'), sign_up_completed_path, \ class: 'btn btn-primary btn-wide' diff --git a/bin/docker_build b/bin/docker_build new file mode 100755 index 00000000000..54c3987a8ba --- /dev/null +++ b/bin/docker_build @@ -0,0 +1,31 @@ +#!/usr/bin/env ruby +require 'pathname' + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path("../../", __FILE__) + +def run(command) + puts("* Running command: #{command}") + abort "command failed (#{$?}): #{command}" unless system command +end + +Dir.chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file: + + puts %q[ + _ _ + | | (_) + | | ___ __ _ _ _ __ __ _ _____ __ + | |/ _ \ / _` | | '_ \ / _` |/ _ \ \ / / + | | (_) | (_| | | | | || (_| | (_) \ V / + |_|\___/ \__, |_|_| |_(_)__, |\___/ \_/ + __/ | __/ | + |___/ |___/ + ] + + # Build and tag the base production image + puts "== Building the base/production Docker image ==" + run "docker build . -f production.Dockerfile -t identity-idp-production" + +end diff --git a/bin/docker_setup b/bin/docker_setup index d3121bbba68..84979c286e5 100755 --- a/bin/docker_setup +++ b/bin/docker_setup @@ -27,6 +27,9 @@ Dir.chdir APP_ROOT do # This file is intended to run after `docker-compose up` # it runs commands that won't work at build time and therefore must be runuted at runtime. + puts "== Shut down cluster ==" + run "docker-compose down" + # The app dir is mounted as a volume, so new files also appear in the container. # files copied during build can be overwritten by the mounted Volume. puts "== Copying new application.yml if file does not already exist" @@ -53,14 +56,16 @@ Dir.chdir APP_ROOT do run "cp config/service_providers.localdev.yml config/service_providers.yml" puts "== Creating and migrating dev database ==" - run "docker-compose run --rm web rake db:create" + run "docker-compose run --rm web bundle exec rake db:create" # The following pattern prevents a database reset from happening in prod. - run "docker-compose run --rm web rake db:environment:set" - run "docker-compose run --rm web rake db:reset" - run "docker-compose run --rm web rake db:environment:set" + run "docker-compose run --rm web bundle exec rake db:environment:set" + run "docker-compose run --rm web bundle exec rake db:reset" + run "docker-compose run --rm web bundle exec rake db:environment:set" # This populates the dev database with sample data - run "docker-compose run --rm web rake dev:prime" + run "docker-compose run --rm web bundle exec rake dev:prime" # Create all parallell test databases - run "docker-compose run --rm web rake parallel:setup" + run "docker-compose run --rm web bundle exec rake parallel:setup" + puts "== Shut down cluster ==" + run "docker-compose down" end diff --git a/config/environments/test.rb b/config/environments/test.rb index 0086488915b..d07b28a834c 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -28,7 +28,10 @@ config.assets.digest = ENV.key?('RAILS_DISABLE_ASSET_DIGEST') ? false : true config.middleware.use RackSessionAccess::Middleware - config.lograge.enabled = true + + # Disable lograge when computing coverage and not in CircleCI, where lograge is required. + # This enables scanning for view test coverage with `rake test:scan_log_for_render` + config.lograge.enabled = !ENV['COVERAGE'] || ENV['CI'] config.after_initialize do # Having bullet enabled in the test environment causes issues with unit diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index aa7a06c1428..f37698ff124 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -66,6 +66,14 @@ callback: -> { Reports::SpUserCountsReport.new.call }, ) +# Send Sp User Quotas Report to S3 +JobRunner::Runner.add_config JobRunner::JobConfiguration.new( + name: 'SP user quotas report', + interval: 24 * 60 * 60, # 24 hours + timeout: 300, + callback: -> { Reports::SpUserQuotasReport.new.call }, +) + # Send Doc Auth Funnel Report to S3 JobRunner::Runner.add_config JobRunner::JobConfiguration.new( name: 'Doc Auth Funnel Report', @@ -113,3 +121,11 @@ timeout: 300, callback: -> { Reports::TotalSpCostReport.new.call }, ) + +# SP Active Users Report to S3 +JobRunner::Runner.add_config JobRunner::JobConfiguration.new( + name: 'SP active users report', + interval: 24 * 60 * 60, # 24 hours + timeout: 300, + callback: -> { Reports::SpActiveUsersReport.new.call }, +) diff --git a/config/locales/help_text/en.yml b/config/locales/help_text/en.yml index d7505eb8586..257357c4479 100644 --- a/config/locales/help_text/en.yml +++ b/config/locales/help_text/en.yml @@ -15,4 +15,6 @@ en: outro_html: "%{sp} will only use this information to connect to your account" phone: Phone number social_security_number: Social Security Number + verified_at: Updated on + verified_at_blank: Not yet verified x509_subject: PIV/CAC Identity diff --git a/config/locales/help_text/es.yml b/config/locales/help_text/es.yml index e797e3a1f5c..e6d2d316e34 100644 --- a/config/locales/help_text/es.yml +++ b/config/locales/help_text/es.yml @@ -15,4 +15,6 @@ es: outro_html: "%{sp} solo usará esta información para conectarse a su cuenta" phone: Teléfono social_security_number: Número de Seguro Social + verified_at: Actualizado en + verified_at_blank: Aún no verificado x509_subject: Identidad PIV/CAC diff --git a/config/locales/help_text/fr.yml b/config/locales/help_text/fr.yml index c243630722b..ecb76a73878 100644 --- a/config/locales/help_text/fr.yml +++ b/config/locales/help_text/fr.yml @@ -16,4 +16,6 @@ fr: à votre compte" phone: Numéro de téléphone social_security_number: Numéro de sécurité sociale + verified_at: Mis à jour le + verified_at_blank: Pas encore vérifié x509_subject: Identité associée à la carte PIV/CAC diff --git a/config/locales/sign_up/en.yml b/config/locales/sign_up/en.yml index 902fce3723f..e3822141bb5 100644 --- a/config/locales/sign_up/en.yml +++ b/config/locales/sign_up/en.yml @@ -1,6 +1,7 @@ --- en: sign_up: + agree_and_continue: Agree and continue cancel: success: Your account has been deleted. We did not save your information. warning_header: If you cancel now diff --git a/config/locales/sign_up/es.yml b/config/locales/sign_up/es.yml index 36bf0c53b37..345dfe9a732 100644 --- a/config/locales/sign_up/es.yml +++ b/config/locales/sign_up/es.yml @@ -1,6 +1,7 @@ --- es: sign_up: + agree_and_continue: Aceptar y continuar cancel: success: Su cuenta ha sido eliminada. No guardamos su información. warning_header: Si cancela ahora diff --git a/config/locales/sign_up/fr.yml b/config/locales/sign_up/fr.yml index e3d417332bf..d0fdc64c113 100644 --- a/config/locales/sign_up/fr.yml +++ b/config/locales/sign_up/fr.yml @@ -1,6 +1,7 @@ --- fr: sign_up: + agree_and_continue: Acceptez et continuez cancel: success: Le dossier contenant votre information a été effacé warning_header: Si vous annulez maintenant diff --git a/config/locales/titles/en.yml b/config/locales/titles/en.yml index 1fa2deaf248..b0602c0d733 100644 --- a/config/locales/titles/en.yml +++ b/config/locales/titles/en.yml @@ -48,6 +48,7 @@ en: completion_html: You've %{accent} with %{app} loa1: created an account new_sp: You are now logging in for the first time + refresh_consent: It's been a year since you gave us consent to share your information verified: You've verified your identity with %{app} totp_setup: new: Set up two-factor authentication diff --git a/config/locales/titles/es.yml b/config/locales/titles/es.yml index b20d14c9518..42bad13beb4 100644 --- a/config/locales/titles/es.yml +++ b/config/locales/titles/es.yml @@ -49,6 +49,8 @@ es: completion_html: Tiene %{accent} con %{app} loa1: creó una cuenta new_sp: Acabas de iniciar sesión por primera vez + refresh_consent: Ha pasado un año desde que nos dio su consentimiento para compartir + su información verified: Tiene verificó su identidad con %{app} totp_setup: new: Configure la autenticación de dos factores diff --git a/config/locales/titles/fr.yml b/config/locales/titles/fr.yml index d20fe992cbe..33fb1a15de8 100644 --- a/config/locales/titles/fr.yml +++ b/config/locales/titles/fr.yml @@ -48,6 +48,8 @@ fr: completion_html: Vous avez %{accent} avec %{app} loa1: créé un compte new_sp: Vous vous connectez pour la première fois + refresh_consent: Cela fait un an que vous nous avez donné votre consentement + pour partager vos informations verified: Vous avez verifié votre identité avec %{app} totp_setup: new: Configurer l'authentification à deux facteurs diff --git a/config/service_providers.localdev.yml b/config/service_providers.localdev.yml index 57e8c257721..3c6709d5164 100644 --- a/config/service_providers.localdev.yml +++ b/config/service_providers.localdev.yml @@ -40,6 +40,46 @@ test: - zipcode allow_prompt_login: true + 'test_saml_sp_not_requesting_signed_response_message': + agency_id: 2 + acs_url: 'http://example.com/test/saml/decode_assertion' + assertion_consumer_logout_service_url: 'http://example.com/test/saml/decode_slo_request' + sp_initiated_login_url: 'https://example.com/auth/saml/login' + failure_to_proof_url: 'https://example.com/' + redirect_uris: + - 'http://example.com/' + - 'http://example.com/auth/result' + - 'http://example.com/logout' + friendly_name: 'Test SP requesting signed response message' + cert: 'saml_test_sp' + logo: 'generic.svg' + ial: 1 + attribute_bundle: + - email + allow_prompt_login: true + block_encryption: 'none' + signed_response_message_requested: false + + 'test_saml_sp_requesting_signed_response_message': + agency_id: 2 + acs_url: 'http://example.com/test/saml/decode_assertion' + assertion_consumer_logout_service_url: 'http://example.com/test/saml/decode_slo_request' + sp_initiated_login_url: 'https://example.com/auth/saml/login' + failure_to_proof_url: 'https://example.com/' + redirect_uris: + - 'http://example.com/' + - 'http://example.com/auth/result' + - 'http://example.com/logout' + friendly_name: 'Test SP requesting signed response message' + cert: 'saml_test_sp' + logo: 'generic.svg' + ial: 1 + attribute_bundle: + - email + allow_prompt_login: true + block_encryption: 'none' + signed_response_message_requested: true + 'https://rp2.serviceprovider.com/auth/saml/metadata': acs_url: 'http://example.com/test/saml/decode_assertion' assertion_consumer_logout_service_url: 'http://example.com/test/saml/decode_slo_request' diff --git a/db/migrate/20200210235313_drop_service_provider_requests.rb b/db/migrate/20200210235313_drop_service_provider_requests.rb new file mode 100644 index 00000000000..0e22ab6a453 --- /dev/null +++ b/db/migrate/20200210235313_drop_service_provider_requests.rb @@ -0,0 +1,7 @@ +class DropServiceProviderRequests < ActiveRecord::Migration[5.1] + def change + safety_assured do + drop_table :service_provider_requests + end + end +end diff --git a/db/migrate/20200220230641_add_ial2_quota_to_service_providers.rb b/db/migrate/20200220230641_add_ial2_quota_to_service_providers.rb new file mode 100644 index 00000000000..0e6181e56e9 --- /dev/null +++ b/db/migrate/20200220230641_add_ial2_quota_to_service_providers.rb @@ -0,0 +1,5 @@ +class AddIal2QuotaToServiceProviders < ActiveRecord::Migration[5.1] + def change + add_column :service_providers, :ial2_quota, :integer + end +end diff --git a/db/migrate/20200220235113_add_verified_at_to_identities.rb b/db/migrate/20200220235113_add_verified_at_to_identities.rb new file mode 100644 index 00000000000..b14f03fd7d3 --- /dev/null +++ b/db/migrate/20200220235113_add_verified_at_to_identities.rb @@ -0,0 +1,5 @@ +class AddVerifiedAtToIdentities < ActiveRecord::Migration[5.1] + def change + add_column :identities, :verified_at, :datetime + end +end diff --git a/db/migrate/20200221215702_add_signed_response_message_requested_to_service_provider.rb b/db/migrate/20200221215702_add_signed_response_message_requested_to_service_provider.rb new file mode 100644 index 00000000000..55943c3d646 --- /dev/null +++ b/db/migrate/20200221215702_add_signed_response_message_requested_to_service_provider.rb @@ -0,0 +1,10 @@ +class AddSignedResponseMessageRequestedToServiceProvider < ActiveRecord::Migration[5.1] + def up + add_column :service_providers, :signed_response_message_requested, :boolean + change_column_default :service_providers, :signed_response_message_requested, false + end + + def down + remove_column :service_providers, :signed_response_message_requested + end +end diff --git a/db/migrate/20200303202931_add_last_consented_at_to_identities.rb b/db/migrate/20200303202931_add_last_consented_at_to_identities.rb new file mode 100644 index 00000000000..65193192ae6 --- /dev/null +++ b/db/migrate/20200303202931_add_last_consented_at_to_identities.rb @@ -0,0 +1,5 @@ +class AddLastConsentedAtToIdentities < ActiveRecord::Migration[5.1] + def change + add_column :identities, :last_consented_at, :datetime, null: true + end +end diff --git a/db/migrate/20200305201944_add_ial_at_to_identities.rb b/db/migrate/20200305201944_add_ial_at_to_identities.rb new file mode 100644 index 00000000000..32c64c90118 --- /dev/null +++ b/db/migrate/20200305201944_add_ial_at_to_identities.rb @@ -0,0 +1,6 @@ +class AddIalAtToIdentities < ActiveRecord::Migration[5.1] + def change + add_column :identities, :last_ial1_authenticated_at, :datetime + add_column :identities, :last_ial2_authenticated_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 4c453c3b69e..814242b3f48 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: 20200115142141) do +ActiveRecord::Schema.define(version: 20200305201944) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -227,6 +227,10 @@ t.string "code_challenge" t.string "rails_session_id" t.json "verified_attributes" + t.datetime "verified_at" + t.datetime "last_consented_at" + t.datetime "last_ial1_authenticated_at" + t.datetime "last_ial2_authenticated_at" t.index ["access_token"], name: "index_identities_on_access_token", unique: true t.index ["session_uuid"], name: "index_identities_on_session_uuid", unique: true t.index ["user_id", "service_provider"], name: "index_identities_on_user_id_and_service_provider", unique: true @@ -372,17 +376,6 @@ t.index ["name"], name: "index_remote_settings_on_name", unique: true end - create_table "service_provider_requests", force: :cascade do |t| - t.string "issuer", null: false - t.string "loa", null: false - t.string "url", null: false - t.string "uuid", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "requested_attributes", default: [], array: true - t.index ["uuid"], name: "index_service_provider_requests_on_uuid", unique: true - end - create_table "service_providers", force: :cascade do |t| t.string "issuer", null: false t.string "friendly_name" @@ -415,6 +408,8 @@ t.string "push_notification_url" t.jsonb "help_text", default: {"sign_in"=>{}, "sign_up"=>{}, "forgot_password"=>{}} t.boolean "allow_prompt_login", default: false + t.boolean "signed_response_message_requested", default: false + t.integer "ial2_quota" t.index ["issuer"], name: "index_service_providers_on_issuer", unique: true end diff --git a/development.Dockerfile b/development.Dockerfile new file mode 100644 index 00000000000..09a2e68ab5b --- /dev/null +++ b/development.Dockerfile @@ -0,0 +1,35 @@ +# This is the image we run in our local development docker-compose cluster +# it is built on top of the local production.Dockerfile +FROM identity-idp-production + +# Use root for more configuration +USER root + +# Re-install dev dependencies +RUN apt-get update \ + && apt-get install -y \ + build-essential \ + git \ + liblzma-dev \ + patch \ + ruby-dev \ + wget + +# Install Chrome for integration tests +RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - +RUN sh -c 'echo "deb https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' +RUN apt-get update +RUN apt-get install -y google-chrome-stable + +# Everything happens here from now on +WORKDIR /upaya + +# Remove vendored gems from base image +RUN rm -rf vendor/bundle +# Install dev and test gems on the system +RUN bundle install --system --with development test + +# Change back to appuser to run the app +USER appuser + +# CMD and EXPOSE are inherited from the base image \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4014f81aaeb..dc1fb86b804 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,12 @@ +# Docker Compose def for local development version: '3' services: web: - build: . + build: + context: . + dockerfile: development.Dockerfile volumes: - - .:/upaya + - .:/upaya ports: - "3000:3000" environment: @@ -14,6 +17,7 @@ services: # Set database timeouts to 30 seconds database_timeout: '30000' database_statement_timeout: '30000' + RACK_TIMEOUT_SERVICE_TIMEOUT: '30000' DOCKER_DB_HOST: 'db' DOCKER_DB_USER: 'postgres' # '' == 1 thread for tests; performs better in a container @@ -24,11 +28,14 @@ services: - redis - mailcatcher db: - image: postgres + image: postgres:9.6-alpine volumes: - ./postgres-data:/var/lib/postgresql/data + # Trust Docker network - Not suitable for production + environment: + POSTGRES_HOST_AUTH_METHOD: 'trust' redis: - image: redis + image: redis:5-alpine mailcatcher: image: rordi/docker-mailcatcher container_name: mailcatcher diff --git a/docs/Docker.md b/docs/Docker.md new file mode 100644 index 00000000000..4235ecdb87b --- /dev/null +++ b/docs/Docker.md @@ -0,0 +1,35 @@ +# Docker + +## Run the app locally with Docker + +1. Download, install, and launch [Docker](https://www.docker.com/products/docker-desktop). You may need to increase memory resources in Docker above the defaults to avoid timeouts. + +1. Build the production IDP image, which serves as a base for the development container: `bin/docker_build` + +1. Build the development Docker containers: `docker-compose build` + +1. Run `make docker_setup` to copy configuration files and bootstrap the database. + +1. Start the Docker containers `docker-compose up` and `open http://localhost:3000` + +Please note that the `docker_setup` script will destroy and re-create configuration files that were previously symlinked. See the script source for more info. + +If `Gemfile` or `package.json` change, you'll need to `docker-compose build` again to install those new dependencies. + +## More useful Docker commands: + +* Run migrations: `docker-compose run --rm web bundle exec rails db:migrate` +* Force the images to re-build: `docker-compose build --no-cache`. You might have to do this if a "regular build" doesn't seem to correctly install new dependencies. +* Stop the containers: `docker-compose stop` +* Stop and remove the containers (`-v` removes Volumes, which includes Postgres data): `docker-compose down` +* Open a shell in a one-off web container: `docker-compose run --rm web bash` +* Open a shell in the running web container: `docker-compose exec web bash` +* Open a shell in the running web container as root: `docker-compose exec --user=root web bash` +* Open a psql shell in the running db container: `docker-compose exec db psql -U postgres` +* `docker system prune` to remove dangling images and free up disk space + +## Running Tests in Docker + +* After Docker is set up you can run the entire suite with `docker-compose run web bundle exec rspec`. This takes a while. +* You can run a one-off test with `docker-compose run web bundle exec rspec spec/file.rb` +* If the cluster is already running you can run the test on those containers using `exec` instead of `run`: `docker-compose exec web bundle exec rspec spec/file.rb` \ No newline at end of file diff --git a/lib/feature_management.rb b/lib/feature_management.rb index 282210dcd05..f2b9db2314a 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -1,4 +1,8 @@ class FeatureManagement + ENVS_DO_NOT_DISPLAY_FAKE_BANNER = %w[ + idp.staging.login.gov secure.login.gov + ].freeze + ENVS_WHERE_PREFILLING_OTP_ALLOWED = %w[ idp.dev.login.gov idp.pt.login.gov idp.dev.identitysandbox.gov idp.pt.identitysandbox.gov ].freeze @@ -70,7 +74,7 @@ def self.current_env_allowed_to_see_usps_code? end def self.fake_banner_mode? - Rails.env.production? && Figaro.env.domain_name != 'secure.login.gov' + Rails.env.production? && ENVS_DO_NOT_DISPLAY_FAKE_BANNER.exclude?(Figaro.env.domain_name) end def self.enable_saml_cert_rotation? diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake new file mode 100644 index 00000000000..107b3ad1d7f --- /dev/null +++ b/lib/tasks/test.rake @@ -0,0 +1,16 @@ +namespace :test do + # Use `> log/test.log` to empty test.log before re-running tests to get an accurate list + # Note you must run tests with `COVERAGE=true` to generate scannable logs. + desc 'Scan test.log for rendered views' + task scan_log_for_render: :environment do + # match 'Rendered two_factor_authentication/otp_verification/show.html.erb' + # Rendered + space + word + [/ + non-whitespace](any number of times) + regex_finder = %r{Rendered\s\w*(/\S*)*} + results = [] + File.readlines('log/test.log').each do |line| + results.push(line.match(regex_finder)) + end + + puts results.map(&:to_s).compact.sort.uniq + end +end diff --git a/package.json b/package.json index ebf63751c33..98ebd3490f2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "npm": "6.x.x" }, "scripts": { - "test": "NODE_ENV=test `npm bin`/mocha --require @babel/register --require spec/javascripts/spec_helper.js spec/javascripts/**/**_spec.js", + "test": "NODE_ENV=test `npm bin`/mocha --require @babel/register --require spec/javascripts/spec_helper.js 'spec/javascripts/**/**_spec.js'", "build": "true" }, "dependencies": { diff --git a/production.Dockerfile b/production.Dockerfile new file mode 100644 index 00000000000..888168f51cb --- /dev/null +++ b/production.Dockerfile @@ -0,0 +1,76 @@ +# Use the official Ruby image because the Rails images have been deprecated +FROM ruby:2.5-slim + +# Set necessary ENV +ENV LC_ALL=C.UTF-8 + +# Enable package fetch over https and add a few core tools +RUN apt-get update \ + && apt-get install -y \ + apt-transport-https \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install Postgres client +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + postgresql-client \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Node 12.x +RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \ + && apt-get update \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* \ + && ln -s ../node/bin/node /usr/local/bin/ \ + && ln -s ../node/bin/npm /usr/local/bin/ + +# Install Yarn +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ + && apt-get update \ + && apt-get install yarn \ + && rm -rf /var/lib/apt/lists/* + +# Everything happens here from now on +WORKDIR /upaya + +# Simple Gem cache. Success here creates a new layer in the image. +# Note - Installs build related debs then removes after use +COPY Gemfile Gemfile.lock ./ +RUN apt-get update \ + && apt-get install -y \ + build-essential \ + git \ + liblzma-dev \ + patch \ + ruby-dev \ + && gem install bundler --conservative \ + && bundle install --deployment --without development test \ + && apt-get remove -y \ + build-essential \ + git \ + liblzma-dev \ + patch \ + ruby-dev \ + && apt autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +# Simple npm cache. Success here creates a new layer in the image. +COPY package.json yarn.lock ./ +RUN NODE_ENV=development yarn install --force + +# Copy in whole source (minus items matched in .dockerignore) +COPY . . + +# Add application user and fix perms +RUN groupadd -r appuser \ + && useradd --system --create-home --gid appuser appuser \ + && chown -R appuser.appuser /upaya + +# Up to this point we've been root, change to a lower priv. user +USER appuser + +EXPOSE 3000 +CMD ["bundle", "exec", "rackup", "config.ru", "--host", "0.0.0.0", "--port", "3000"] diff --git a/spec/config/initializers/job_configurations.rb b/spec/config/initializers/job_configurations.rb index e78bd782bd7..583b8a9cad3 100644 --- a/spec/config/initializers/job_configurations.rb +++ b/spec/config/initializers/job_configurations.rb @@ -99,6 +99,18 @@ expect(job.callback.call).to eq 'the report test worked' end + it 'runs the sp user quotas report job' do + job = JobRunner::Runner.configurations.find { |c| c.name == 'SP user quotas report' } + expect(job).to be_instance_of(JobRunner::JobConfiguration) + expect(job.interval).to eq 24 * 60 * 60 + + service = instance_double(Reports::SpUserQuotasReport) + expect(Reports::SpUserQuotasReport).to receive(:new).and_return(service) + expect(service).to receive(:call).and_return('the report test worked') + + expect(job.callback.call).to eq 'the report test worked' + end + it 'runs the sp success rate report job' do job = JobRunner::Runner.configurations.find { |c| c.name == 'SP success rate report' } expect(job).to be_instance_of(JobRunner::JobConfiguration) @@ -164,5 +176,19 @@ expect(job.callback.call).to eq 'the report test worked' end + + it 'runs the SP active users report job' do + job = JobRunner::Runner.configurations.find do |c| + c.name == 'SP active users report' + end + expect(job).to be_instance_of(JobRunner::JobConfiguration) + expect(job.interval).to eq 24 * 60 * 60 + + service = instance_double(Reports::SpActiveUsersReport) + expect(Reports::SpActiveUsersReport).to receive(:new).and_return(service) + expect(service).to receive(:call).and_return('the report test worked') + + expect(job.callback.call).to eq 'the report test worked' + end end end diff --git a/spec/controllers/concerns/verify_sp_attributes_concern_spec.rb b/spec/controllers/concerns/verify_sp_attributes_concern_spec.rb new file mode 100644 index 00000000000..834b9782b37 --- /dev/null +++ b/spec/controllers/concerns/verify_sp_attributes_concern_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' + +RSpec.describe VerifySPAttributesConcern do + controller ApplicationController do + # ApplicationController already includes VerifySPAttributesConcern + end + + describe '#consent_has_expired?' do + let(:sp_session_identity) { build(:identity) } + + before do + allow(controller).to receive(:sp_session_identity).and_return(sp_session_identity) + end + + subject(:consent_has_expired?) { controller.consent_has_expired? } + + context 'when there is no sp_session_identity' do + let(:sp_session_identity) { nil } + it 'is false' do + expect(consent_has_expired?).to eq(false) + end + end + + context 'when there is no last_consented_at' do + it 'is true' do + expect(consent_has_expired?).to eq(true) + end + end + + context 'when last_consented_at within one year' do + let(:sp_session_identity) { build(:identity, last_consented_at: 5.days.ago) } + + it 'is false' do + expect(consent_has_expired?).to eq(false) + end + end + + context 'when the last_consented_at is older than a year ago' do + let(:sp_session_identity) { build(:identity, last_consented_at: 2.years.ago) } + + it 'is true' do + expect(consent_has_expired?).to eq(true) + end + end + + context 'when last_consented_at is nil but created_at is within a year' do + let(:sp_session_identity) do + build(:identity, last_consented_at: nil, created_at: 4.days.ago) + end + + it 'is false' do + expect(consent_has_expired?).to eq(false) + end + end + + context 'when last_consented_at is nil and created_at is older than a year' do + let(:sp_session_identity) do + build(:identity, last_consented_at: nil, created_at: 4.years.ago) + end + + it 'is true' do + expect(consent_has_expired?).to eq(true) + end + end + end +end diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index ca81e6340e2..eccc51cc9df 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -71,7 +71,7 @@ it 'redirects to the redirect_uri immediately when pii is unlocked' do IdentityLinker.new(user, client_id).link_identity(ial: 3) user.identities.last.update!( - verified_attributes: %w[given_name family_name birthdate], + verified_attributes: %w[given_name family_name birthdate verified_at], ) allow(controller).to receive(:pii_requested_but_locked?).and_return(false) action @@ -82,7 +82,7 @@ it 'redirects to the password capture url when pii is locked' do IdentityLinker.new(user, client_id).link_identity(ial: 3) user.identities.last.update!( - verified_attributes: %w[given_name family_name birthdate], + verified_attributes: %w[given_name family_name birthdate verified_at], ) allow(controller).to receive(:pii_requested_but_locked?).and_return(true) action diff --git a/spec/controllers/saml_signed_message_spec.rb b/spec/controllers/saml_signed_message_spec.rb new file mode 100644 index 00000000000..f61d9d3dde7 --- /dev/null +++ b/spec/controllers/saml_signed_message_spec.rb @@ -0,0 +1,65 @@ +require 'rails_helper' + +describe SamlIdpController do + include SamlAuthHelper + + before do + # All the tests here were written prior to the interstitial + # authorization confirmation page so let's force the system + # to skip past that page + allow(controller).to receive(:auth_count).and_return(2) + end + + render_views + + describe 'GET /api/saml/auth' do + context "SP's can have signed_response_message_requested set" do + let(:user) { create(:user, :signed_up) } + let(:saml_response_encoded) do + Nokogiri::HTML(response.body).css('#SAMLResponse').first.attributes['value'].to_s + end + let(:saml_response_text) { Base64.decode64(saml_response_encoded) } + let(:saml_response) { REXML::Document.new(saml_response_text) } + + context 'with signed_response_message_requested true' do + before do + generate_saml_response(user, sp_requesting_signed_saml_response_settings) + end + + it 'finds Signatures in the message and assertion' do + signature_count = REXML::XPath.match(saml_response, '//ds:Signature').length + + expect(signature_count).to eq 2 + end + + # rubocop:disable Metrics/LineLength + it 'finds a Signature referencing the Response' do + response_id = REXML::XPath.match(saml_response, '//samlp:Response').first.attributes['ID'] + signature_ref = REXML::XPath.match(saml_response, '//ds:Reference').first.attributes['URI'][1..-1] + + expect(signature_ref).to eq response_id + end + end + + context 'with signed_response_message_requested false' do + before do + generate_saml_response(user, sp_not_requesting_signed_saml_response_settings) + end + + it 'only finds one Signature' do + signature_count = REXML::XPath.match(saml_response, '//ds:Signature').length + + expect(signature_count).to eq 1 + end + + it 'only finds a Signature referencing the Assertion' do + assertion_id = REXML::XPath.match(saml_response, '//Assertion').first.attributes['ID'] + signature_ref = REXML::XPath.match(saml_response, '//ds:Reference').first.attributes['URI'][1..-1] + + expect(signature_ref).to eq assertion_id + end + # rubocop:enable Metrics/LineLength + end + end + end +end diff --git a/spec/controllers/sign_up/completions_controller_spec.rb b/spec/controllers/sign_up/completions_controller_spec.rb index a6e4354ebd6..04a5fd06b7c 100644 --- a/spec/controllers/sign_up/completions_controller_spec.rb +++ b/spec/controllers/sign_up/completions_controller_spec.rb @@ -94,6 +94,8 @@ end describe '#update' do + let(:now) { Time.zone.now } + before do stub_analytics allow(@analytics).to receive(:track_event) @@ -128,8 +130,11 @@ request_url: 'http://example.com', requested_attributes: ['email'], } - expect(@linker).to receive(:link_identity).with(ial: 1, verified_attributes: ['email']) - patch :update + expect(@linker).to receive(:link_identity). + with(ial: 1, verified_attributes: ['email'], last_consented_at: now) + Timecop.freeze(now) do + patch :update + end end end @@ -159,11 +164,16 @@ subject.session[:sp] = { ial2: true, request_url: 'http://example.com', - requested_attributes: %w[email first_name], + requested_attributes: %w[email first_name verified_at], } - expect(@linker).to receive(:link_identity). - with(ial: 2, verified_attributes: %w[email first_name]) - patch :update + expect(@linker).to receive(:link_identity).with( + ial: 2, + verified_attributes: %w[email first_name verified_at], + last_consented_at: now, + ) + Timecop.freeze(now) do + patch :update + end end end end diff --git a/spec/features/backup_mfa/sign_up_spec.rb b/spec/features/backup_mfa/sign_up_spec.rb index 0e8a6fed7e8..6039180d74a 100644 --- a/spec/features/backup_mfa/sign_up_spec.rb +++ b/spec/features/backup_mfa/sign_up_spec.rb @@ -26,7 +26,7 @@ expect(page).to have_current_path(sign_up_completed_path) - click_button t('forms.buttons.continue') + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') expect(user.reload.encrypted_recovery_code_digest).to be_empty diff --git a/spec/features/idv/uak_password_spec.rb b/spec/features/idv/uak_password_spec.rb index ec646bb8601..f9ed09daebe 100644 --- a/spec/features/idv/uak_password_spec.rb +++ b/spec/features/idv/uak_password_spec.rb @@ -16,7 +16,7 @@ expect(page).to have_current_path(sign_up_completed_path) - click_on t('forms.buttons.continue') + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') end diff --git a/spec/features/multiple_emails/sp_sign_in_spec.rb b/spec/features/multiple_emails/sp_sign_in_spec.rb index 09f754e6820..011babef9d7 100644 --- a/spec/features/multiple_emails/sp_sign_in_spec.rb +++ b/spec/features/multiple_emails/sp_sign_in_spec.rb @@ -14,7 +14,7 @@ signin(email, user.password) fill_in_code_with_last_phone_otp click_submit_default - click_continue if current_path == sign_up_completed_path + click_agree_and_continue if current_path == sign_up_completed_path expect_oidc_sp_to_receive_email(email) @@ -33,7 +33,7 @@ signin(email, user.password) fill_in_code_with_last_phone_otp click_submit_default - click_continue if current_path == sign_up_completed_path + click_agree_and_continue if current_path == sign_up_completed_path xmldoc = SamlResponseDoc.new('feature', 'response_assertion') email_from_saml_response = xmldoc.attribute_value_for('email') diff --git a/spec/features/openid_connect/authorization_confirmation_spec.rb b/spec/features/openid_connect/authorization_confirmation_spec.rb index b51dd5803c2..fde63ed9dd2 100644 --- a/spec/features/openid_connect/authorization_confirmation_spec.rb +++ b/spec/features/openid_connect/authorization_confirmation_spec.rb @@ -15,7 +15,7 @@ def create_user_and_remember_device check :remember_device fill_in_code_with_last_phone_otp click_submit_default - click_continue + click_agree_and_continue visit sign_out_url diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index 65173e0860e..7c43258f127 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -140,6 +140,79 @@ expect(page).to have_content(t('headings.sign_in_without_sp')) end + it 'returns verified_at in an ial1 session if requested', driver: :mobile_rack_test do + user = user_with_2fa + profile = create(:profile, :active, :verified, + pii: { first_name: 'John', ssn: '111223333' }, + user: user) + + token_response = sign_in_get_token_response( + user: user, + scope: 'openid email profile:verified_at', + handoff_page_steps: proc do + expect(page).to have_content(t('help_text.requested_attributes.verified_at')) + + click_agree_and_continue + end, + ) + + access_token = token_response[:access_token] + expect(access_token).to be_present + + page.driver.get api_openid_connect_userinfo_path, + {}, + 'HTTP_AUTHORIZATION' => "Bearer #{access_token}" + + userinfo_response = JSON.parse(page.body).with_indifferent_access + expect(userinfo_response[:email]).to eq(user.email) + expect(userinfo_response[:verified_at]).to eq(profile.verified_at.to_i) + end + + it 'returns a null verified_at if the account has not been proofed', driver: :mobile_rack_test do + token_response = sign_in_get_token_response( + scope: 'openid email profile:verified_at', + handoff_page_steps: proc do + expect(page).to have_content(t('help_text.requested_attributes.verified_at')) + expect(page).to have_content(t('help_text.requested_attributes.verified_at_blank')) + + click_agree_and_continue + end, + ) + + access_token = token_response[:access_token] + expect(access_token).to be_present + + page.driver.get api_openid_connect_userinfo_path, + {}, + 'HTTP_AUTHORIZATION' => "Bearer #{access_token}" + + userinfo_response = JSON.parse(page.body).with_indifferent_access + expect(userinfo_response[:email]).to be_present + expect(userinfo_response[:verified_at]).to be_nil + end + + it 'prompts for consent if last consent time was over a year ago', driver: :mobile_rack_test do + client_id = 'urn:gov:gsa:openidconnect:test' + user = user_with_2fa + link_identity(user, client_id) + + user.identities.last.update( + last_consented_at: 2.years.ago, + created_at: 2.years.ago, + ) + + sign_in_get_id_token( + user: user, + client_id: client_id, + handoff_page_steps: proc do + expect(page).to have_content(t('titles.sign_up.refresh_consent')) + expect(page).to_not have_content(t('titles.sign_up.new_sp')) + + click_agree_and_continue + end, + ) + end + context 'with PKCE', driver: :mobile_rack_test do it 'succeeds with client authentication via PKCE' do client_id = 'urn:gov:gsa:openidconnect:test' @@ -229,7 +302,7 @@ perform_in_browser(:two) do confirm_email_in_a_different_browser(email) - click_button t('forms.buttons.continue') + click_agree_and_continue continue_as(email) redirect_uri = URI(current_url) expect(redirect_uri.to_s).to start_with('gov.gsa.openidconnect.test://result') @@ -260,7 +333,7 @@ expect(current_url).to eq(sign_up_completed_url) expect(page).to have_content(t('titles.sign_up.new_sp')) - click_continue + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') visit sign_out_url visit oidc_path @@ -343,7 +416,7 @@ perform_in_browser(:two) do confirm_email_in_a_different_browser(email) - click_button t('forms.buttons.continue') + click_agree_and_continue expect(current_url).to eq new_user_session_url expect(page). @@ -427,13 +500,19 @@ def visit_idp_from_mobile_app_with_ial1(state: SecureRandom.hex) ) end - def sign_in_get_id_token - client_id = 'urn:gov:gsa:openidconnect:test' + def sign_in_get_id_token(**args) + token_response = sign_in_get_token_response(**args) + token_response[:id_token] + end + + def sign_in_get_token_response( + user: user_with_2fa, scope: 'openid email', handoff_page_steps: nil, + client_id: 'urn:gov:gsa:openidconnect:test' + ) state = SecureRandom.hex 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']) @@ -442,7 +521,7 @@ def sign_in_get_id_token client_id: client_id, response_type: 'code', acr_values: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, - scope: 'openid email', + scope: scope, redirect_uri: 'gov.gsa.openidconnect.test://result', state: state, prompt: 'select_account', @@ -452,6 +531,7 @@ def sign_in_get_id_token ) _user = sign_in_live_with_2fa(user) + handoff_page_steps&.call redirect_uri = URI(current_url) redirect_params = Rack::Utils.parse_query(redirect_uri.query).with_indifferent_access @@ -464,8 +544,7 @@ def sign_in_get_id_token code_verifier: code_verifier expect(page.status_code).to eq(200) - token_response = JSON.parse(page.body).with_indifferent_access - token_response[:id_token] + JSON.parse(page.body).with_indifferent_access end def sp_public_key @@ -502,7 +581,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 + click_agree_and_continue_optional redirect_uri = URI(current_url) redirect_params = Rack::Utils.parse_query(redirect_uri.query).with_indifferent_access diff --git a/spec/features/openid_connect/redirect_uri_validation_spec.rb b/spec/features/openid_connect/redirect_uri_validation_spec.rb index c54f1227ef1..72355d6e1a8 100644 --- a/spec/features/openid_connect/redirect_uri_validation_spec.rb +++ b/spec/features/openid_connect/redirect_uri_validation_spec.rb @@ -129,7 +129,7 @@ fill_in_credentials_and_submit(user.email, user.password) fill_in_code_with_last_phone_otp click_submit_default - click_continue + click_agree_and_continue redirect_host = URI.parse(current_url).host redirect_scheme = URI.parse(current_url).scheme diff --git a/spec/features/personal_key_upgrade_spec.rb b/spec/features/personal_key_upgrade_spec.rb index 74682f20b63..c4e6b7fb74a 100644 --- a/spec/features/personal_key_upgrade_spec.rb +++ b/spec/features/personal_key_upgrade_spec.rb @@ -107,7 +107,7 @@ def expect_user_to_go_to_sp expect(page).to have_content(t('help_text.requested_attributes.intro_html', sp: 'Test SP')) expect(page).to have_current_path(sign_up_completed_path) - click_continue + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') end diff --git a/spec/features/reports/sp_active_users_report_spec.rb b/spec/features/reports/sp_active_users_report_spec.rb new file mode 100644 index 00000000000..1e1ff0e075d --- /dev/null +++ b/spec/features/reports/sp_active_users_report_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +feature 'sp active users report' do + include SamlAuthHelper + include IdvHelper + + it 'reports a user as ial1 active for an ial1 sign in' do + user = create(:user, :signed_up) + visit_idp_from_sp_with_ial1(:oidc) + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + click_agree_and_continue + expect(current_url).to start_with('http://localhost:7654/auth/result') + + results = [{ 'issuer': 'urn:gov:gsa:openidconnect:sp:server', + 'total_ial1_active': 1, + 'total_ial2_active': 0 }].to_json + expect(Db::Identity::SpActiveUserCounts.call('01-01-2019').to_json).to eq(results) + end + + it 'reports a user as ial2 active for an ial2 sign in' do + user = create(:profile, :active, :verified, + pii: { first_name: 'John', ssn: '111223333' }).user + visit_idp_from_sp_with_ial2(:oidc) + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + click_agree_and_continue + expect(current_url).to start_with('http://localhost:7654/auth/result') + + results = [{ 'issuer': 'urn:gov:gsa:openidconnect:sp:server', + 'total_ial1_active': 0, + 'total_ial2_active': 1 }].to_json + expect(Db::Identity::SpActiveUserCounts.call('01-01-2019').to_json).to eq(results) + end +end diff --git a/spec/features/reports/sp_success_rate_report_spec.rb b/spec/features/reports/sp_success_rate_report_spec.rb index 5772229bade..f6a91f02834 100644 --- a/spec/features/reports/sp_success_rate_report_spec.rb +++ b/spec/features/reports/sp_success_rate_report_spec.rb @@ -44,6 +44,6 @@ def visit_idp_from_sp_and_back_again fill_in_credentials_and_submit(user.email, user.password) fill_in_code_with_last_phone_otp click_submit_default - click_continue + click_agree_and_continue end end diff --git a/spec/features/saml/authorization_confirmation_spec.rb b/spec/features/saml/authorization_confirmation_spec.rb index fe10cc356c7..0f8e434a486 100644 --- a/spec/features/saml/authorization_confirmation_spec.rb +++ b/spec/features/saml/authorization_confirmation_spec.rb @@ -16,7 +16,7 @@ def create_user_and_remember_device fill_in_code_with_last_phone_otp click_submit_default visit saml_authn_request - click_continue + click_agree_and_continue visit sign_out_url user diff --git a/spec/features/saml/ial1_sso_spec.rb b/spec/features/saml/ial1_sso_spec.rb index c536ff7d09c..55d4dac2a16 100644 --- a/spec/features/saml/ial1_sso_spec.rb +++ b/spec/features/saml/ial1_sso_spec.rb @@ -28,7 +28,7 @@ to_not have_content t('help_text.requested_attributes.social_security_number') end - click_on t('forms.buttons.continue') + click_agree_and_continue continue_as(email) @@ -45,7 +45,7 @@ fill_in_credentials_and_submit(user.email, user.password) fill_in_code_with_last_phone_otp click_submit_default - click_continue + click_agree_and_continue expect(current_url).to eq saml_authn_request @@ -92,7 +92,7 @@ fill_in_credentials_and_submit(user.email, user.password) fill_in_code_with_last_phone_otp click_submit_default - click_continue + click_agree_and_continue expect(current_url).to eq saml_authn_request end @@ -115,12 +115,12 @@ end it 'returns to sp after clicking continue' do - click_continue + click_agree_and_continue expect(current_url).to eq(saml_authn_request) end it 'it confirms the user wants to continue to the SP after signing in again' do - click_continue + click_agree_and_continue set_new_browser_session @@ -201,4 +201,27 @@ expect(page).to have_content t('links.back_to_sp', sp: sp.friendly_name) end end + + context 'requesting verified_at for an IAL1 account' do + it 'shows verified_at as a requested attribute, even if blank' do + user = create(:user, :signed_up) + saml_authn_request = auth_request.create(ial1_with_verified_at_saml_settings) + + visit saml_authn_request + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + + expect(current_url).to match new_user_session_path + expect(page).to have_content(t('help_text.requested_attributes.verified_at')) + expect(page).to have_content(t('help_text.requested_attributes.verified_at_blank')) + + click_agree_and_continue + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + + expect(xmldoc.attribute_node_for('verified_at')).to be_present + expect(xmldoc.attribute_value_for('verified_at')).to be_blank + end + end end diff --git a/spec/features/saml/multiple_endpoints_spec.rb b/spec/features/saml/multiple_endpoints_spec.rb index 296569c2438..b33de99bdc0 100644 --- a/spec/features/saml/multiple_endpoints_spec.rb +++ b/spec/features/saml/multiple_endpoints_spec.rb @@ -25,7 +25,7 @@ it 'creates a valid auth request' do sign_in_and_2fa_user(user) visit endpoint_authn_request - click_continue + click_agree_and_continue response_node = page.find('#SAMLResponse', visible: false) decoded_response = Base64.decode64(response_node.value) @@ -43,7 +43,7 @@ it 'create a valid logout request' do sign_in_and_2fa_user(user) visit endpoint_authn_request - click_continue + click_agree_and_continue service_provider = ServiceProvider.from_issuer(endpoint_saml_settings.issuer) uuid = user.decorate.active_identity_for(service_provider).uuid diff --git a/spec/features/saml/redirect_uri_validation_spec.rb b/spec/features/saml/redirect_uri_validation_spec.rb index 4b137be7e31..0558235a667 100644 --- a/spec/features/saml/redirect_uri_validation_spec.rb +++ b/spec/features/saml/redirect_uri_validation_spec.rb @@ -17,7 +17,7 @@ fill_in_credentials_and_submit(user.email, user.password) fill_in_code_with_last_phone_otp click_submit_default - click_continue + click_agree_and_continue click_submit_default expect(current_url).to eq sp.acs_url diff --git a/spec/features/saml/saml_spec.rb b/spec/features/saml/saml_spec.rb index 298aad63d3b..dcc0817bcc0 100644 --- a/spec/features/saml/saml_spec.rb +++ b/spec/features/saml/saml_spec.rb @@ -47,7 +47,7 @@ class MockSession; end before do sign_in_and_2fa_user(user) visit sp1_authnrequest - click_continue + click_agree_and_continue end let(:xmldoc) { SamlResponseDoc.new('feature', 'response_assertion') } @@ -61,7 +61,7 @@ class MockSession; end before do sign_in_and_2fa_user(user) visit authnrequest_get - click_continue + click_agree_and_continue end let(:xmldoc) { SamlResponseDoc.new('feature', 'response_assertion') } diff --git a/spec/features/sp_cost_tracking_spec.rb b/spec/features/sp_cost_tracking_spec.rb index c549d9edcc6..83b8b1203bd 100644 --- a/spec/features/sp_cost_tracking_spec.rb +++ b/spec/features/sp_cost_tracking_spec.rb @@ -49,7 +49,8 @@ click_submit_default expect_sp_cost_type(0, 1, 'digest') - expect_sp_cost_type(1, 1, 'authentication') + expect_sp_cost_type(1, 1, 'sms') + expect_sp_cost_type(2, 1, 'authentication') end it 'logs the correct costs for an ial2 authentication' do @@ -65,7 +66,8 @@ click_submit_default expect_sp_cost_type(0, 2, 'digest') - expect_sp_cost_type(1, 2, 'authentication') + expect_sp_cost_type(1, 2, 'sms') + expect_sp_cost_type(2, 2, 'authentication') end it 'logs the correct costs for a direct authentication' do diff --git a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb index aefb6cfb7b5..3323805fda6 100644 --- a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb +++ b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb @@ -73,7 +73,7 @@ expect(page).to have_current_path(sign_up_completed_path) - click_continue + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') end diff --git a/spec/features/users/password_recovery_via_recovery_code_spec.rb b/spec/features/users/password_recovery_via_recovery_code_spec.rb index 87ad8c355ac..b06ac1fb6d3 100644 --- a/spec/features/users/password_recovery_via_recovery_code_spec.rb +++ b/spec/features/users/password_recovery_via_recovery_code_spec.rb @@ -47,7 +47,7 @@ click_idv_continue complete_idv_profile_ok(user, new_password) acknowledge_and_confirm_personal_key - click_continue + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index 740b2f6a19d..d42fbee7c51 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -147,6 +147,7 @@ click_submit_default click_continue end + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') end scenario 'user cannot sign in with certificate timeout error' do @@ -706,7 +707,7 @@ fill_in_credentials_and_submit(user.email, user.password) fill_in_code_with_last_phone_otp click_submit_default - click_continue + click_agree_and_continue redirect_uri = URI(current_url) @@ -721,7 +722,7 @@ fill_in_credentials_and_submit(user.email, user.password) fill_in_code_with_last_phone_otp click_submit_default - click_continue + click_agree_and_continue visit_idp_from_oidc_sp_with_loa1_prompt_login expect(current_path).to eq(bounced_path) @@ -786,7 +787,7 @@ expect(current_path).to eq sign_up_completed_path expect(page).to have_content(user.email) - click_continue + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') end @@ -802,7 +803,7 @@ expect(current_path).to eq sign_up_completed_path expect(page).to have_content('111223333') - click_continue + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') end @@ -819,7 +820,7 @@ expect(current_path).to eq sign_up_completed_path expect(page).to have_content(user.email) - click_continue + click_agree_and_continue expect(current_url).to eq @saml_authn_request end @@ -835,12 +836,58 @@ expect(current_path).to eq sign_up_completed_path expect(page).to have_content('111223333') - click_continue + click_agree_and_continue expect(current_url).to eq @saml_authn_request end end + context 'ial2 param on sign up screen' do + before do + enable_doc_auth + visit root_path(ial: 2) + end + + it 'invokes ial2 flow if the user already has an ial1 account' do + user = create(:user, :signed_up) + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + + complete_all_doc_auth_steps + click_continue + fill_in 'Password', with: user.password + click_continue + click_acknowledge_personal_key + click_continue + + expect(current_path).to eq(account_path) + end + + it 'invokes ial2 flow if the user does not have an ial1 account' do + register_user('foo@test.com') + + complete_all_doc_auth_steps + click_continue + fill_in 'Password', with: Features::SessionHelper::VALID_PASSWORD + click_continue + click_acknowledge_personal_key + click_continue + + expect(current_path).to eq(account_path) + end + + it 'goes to the account page if the user is already verified' do + user = create(:user, :signed_up) + create(:profile, :active, :verified, pii: { ssn: '1234', dob: '1920-01-01' }, user: user) + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + + expect(current_path).to eq(account_path) + end + end + def perform_steps_to_get_to_add_piv_cac_during_sign_up user = create(:user, :signed_up, :with_phone) visit_idp_from_sp_with_ial1(:oidc) diff --git a/spec/features/users/user_profile_spec.rb b/spec/features/users/user_profile_spec.rb index b86d8d0ba17..989eff572b0 100644 --- a/spec/features/users/user_profile_spec.rb +++ b/spec/features/users/user_profile_spec.rb @@ -162,7 +162,7 @@ expect(current_path).to eq(sign_up_completed_path) - click_continue + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') end diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index d10c45b799d..e8099a185de 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -206,6 +206,19 @@ end end + context 'when scope includes profile:verified_at but the sp is only ial1' do + let(:client_id) { 'urn:gov:gsa:openidconnect:test:loa1' } + let(:acr_values) { Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF } + let(:scope) { 'email profile:verified_at' } + + it 'has errors' do + allow(Figaro.env).to receive(:unauthorized_scope_enabled).and_return('true') + expect(valid?).to eq(false) + expect(form.errors[:scope]). + to include(t('openid_connect.authorization.errors.unauthorized_scope')) + end + end + context 'redirect_uri' do context 'without a redirect_uri' do let(:redirect_uri) { nil } diff --git a/spec/javascripts/app/components/focus-trap-proxy_spec.js b/spec/javascripts/app/components/focus-trap-proxy_spec.js index 6e9ad8d39d3..0db5477ce30 100644 --- a/spec/javascripts/app/components/focus-trap-proxy_spec.js +++ b/spec/javascripts/app/components/focus-trap-proxy_spec.js @@ -5,33 +5,19 @@ const stub = sinon.stub; describe('focusTrap', () => { let proxy; - const fakeFocusTrap = { - build() { - return function() { + + beforeEach(() => { + proxy = proxyquire('../../../../app/javascript/app/components/focus-trap-proxy', { + // Mock external focus-trap library + 'focus-trap': function() { const thisTrap = sinon.createStubInstance(function() {}); thisTrap.deactivate = stub(); thisTrap.activate = stub(); - return thisTrap; - }; - }, - }; - - beforeEach(() => { - proxy = proxyquire('../../../../app/javascript/app/components/focus-trap-proxy', { - // jump through this crazy hoop so we can spy on the method and ensure - // the proxy object is calling the underlying `focusTrap` constructor - 'focus-trap': () => (fakeFocusTrap.build())(), + }, }).default; }); - it('calls the underlying focusTrap object', () => { - sinon.spy(fakeFocusTrap, 'build'); - proxy('foo'); - expect(fakeFocusTrap.build.calledOnce).to.be.true(); - fakeFocusTrap.build.restore(); - }); - context('#deactivate', () => { it('proxies to `deactivate` and reactivates the last active trap', () => { const trapA = proxy('foo1'); diff --git a/spec/javascripts/app/webauthn_spec.js b/spec/javascripts/app/webauthn_spec.js index 70d9ba64f08..f860ac2defd 100644 --- a/spec/javascripts/app/webauthn_spec.js +++ b/spec/javascripts/app/webauthn_spec.js @@ -31,6 +31,12 @@ describe('WebAuthn', () => { }); }); + describe('extractCredentials', () => { + it('returns [] if credentials are empty string', () => { + expect(WebAuthn.extractCredentials('')).to.eql([]); + }); + }); + describe('enrollWebauthnDevice', () => { const userId = '123'; const userEmail = 'test@test.com'; @@ -59,6 +65,10 @@ describe('WebAuthn', () => { timeout: 800000, attestation: 'none', excludeList: [], + authenticatorSelection: { + authenticatorAttachment: 'cross-platform', + userVerification: 'discouraged', + }, excludeCredentials: [ { // encodes to 'credential123' diff --git a/spec/presenters/openid_connect_user_info_presenter_spec.rb b/spec/presenters/openid_connect_user_info_presenter_spec.rb index 206174a5718..c4b0aaf3633 100644 --- a/spec/presenters/openid_connect_user_info_presenter_spec.rb +++ b/spec/presenters/openid_connect_user_info_presenter_spec.rb @@ -5,10 +5,14 @@ let(:rails_session_id) { SecureRandom.uuid } let(:scope) { 'openid email address phone profile social_security_number x509:subject' } + let(:service_provider_ial) { 2 } + let(:service_provider) { create(:service_provider, ial: service_provider_ial) } + let(:profile) { build(:profile, :active, :verified) } let(:identity) do build(:identity, rails_session_id: rails_session_id, - user: create(:user), + user: create(:user, profiles: [profile]), + service_provider: service_provider.issuer, scope: scope) end @@ -111,6 +115,7 @@ region: 'DC', postal_code: '12345', ) + expect(user_info[:verified_at]).to eq(profile.verified_at.to_i) expect(user_info[:social_security_number]).to eq('666661234') end end @@ -135,10 +140,31 @@ expect(user_info[:phone]).to eq('+1 (703) 555-5555') expect(user_info[:phone_verified]).to eq(true) expect(user_info[:address]).to eq(nil) + expect(user_info[:verified_at]).to eq(nil) expect(user_info[:social_security_number]).to eq(nil) end end end + + context 'verified_at' do + let(:scope) { 'openid profile:verified_at' } + + context 'when the service provider has ial1 access' do + let(:service_provider_ial) { 1 } + + it 'does not provide verified_at' do + expect(user_info[:verified_at]).to eq(nil) + end + end + + context 'when the service provider has ial2 access' do + let(:service_provider_ial) { 2 } + + it 'provides verified_at' do + expect(user_info[:verified_at]).to eq(profile.verified_at.to_i) + end + end + end end context 'when the identity only has ial1 access' do diff --git a/spec/presenters/saml_request_presenter_spec.rb b/spec/presenters/saml_request_presenter_spec.rb index 6bd52df0f48..0becc07d7c4 100644 --- a/spec/presenters/saml_request_presenter_spec.rb +++ b/spec/presenters/saml_request_presenter_spec.rb @@ -14,13 +14,13 @@ allow(parser).to receive(:requested_attributes).and_return(nil) all_attributes = %w[ - email first_name middle_name last_name dob ssn + email first_name middle_name last_name dob ssn verified_at phone address1 address2 city state zipcode foo ] service_provider = ServiceProvider.new(attribute_bundle: all_attributes) presenter = SamlRequestPresenter.new(request: request, service_provider: service_provider) - expect(presenter.requested_attributes).to eq(%i[email]) + expect(presenter.requested_attributes).to eq(%i[email verified_at]) end end @@ -34,12 +34,12 @@ service_provider = ServiceProvider.new( attribute_bundle: %w[ - email first_name middle_name last_name dob foo ssn phone + email first_name middle_name last_name dob foo ssn phone verified_at ], ) presenter = SamlRequestPresenter.new(request: request, service_provider: service_provider) valid_attributes = %i[ - email given_name name family_name birthdate social_security_number phone + email given_name name family_name birthdate social_security_number phone verified_at ] expect(presenter.requested_attributes).to eq(valid_attributes) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 859b034959a..5cf7f64fbdf 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,14 +1,4 @@ # This file is copied to spec/ when you run 'rails generate rspec:install' -require 'redis' - -begin - Redis.current.call 'INFO' -rescue Redis::CannotConnectError => cce - puts "\n\nIt appears Redis is not running, but it is required for (some) specs to run\n\n" - puts cce - exit 1 -end - if ENV['COVERAGE'] require 'simplecov' SimpleCov.start 'rails' do @@ -58,6 +48,14 @@ config.before(:suite) do Rails.application.load_seed + + begin + REDIS_POOL.with { |cache| cache.pool.with(&:info) } + rescue RuntimeError => error + puts error + puts 'It appears Redis is not running, but it is required for (some) specs to run' + exit 1 + end end config.before(:each) do diff --git a/spec/services/attribute_asserter_spec.rb b/spec/services/attribute_asserter_spec.rb index 442e3c8feb1..db375e58ea4 100644 --- a/spec/services/attribute_asserter_spec.rb +++ b/spec/services/attribute_asserter_spec.rb @@ -12,10 +12,12 @@ session_uuid: SecureRandom.uuid, ) end + let(:service_provider_ial) { 2 } let(:service_provider) do instance_double( ServiceProvider, issuer: 'http://localhost:3000', + ial: service_provider_ial, metadata: {}, ) end @@ -167,6 +169,31 @@ end end + context 'custom bundle includes verified_at' do + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email verified_at]) + subject.build + end + + context 'the service provider is ial1' do + let(:service_provider_ial) { 1 } + + it 'only includes uuid + email (no verified_at)' do + expect(user.asserted_attributes.keys).to eq %i[uuid email] + end + end + + context 'the service provider is ial2' do + let(:service_provider_ial) { 2 } + + it 'includes verified_at' do + expect(user.asserted_attributes.keys).to eq %i[uuid email verified_at] + end + end + end + context 'Service Provider does not specify bundle' do before do allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). diff --git a/spec/services/db/identity/sp_active_user_counts_spec.rb b/spec/services/db/identity/sp_active_user_counts_spec.rb new file mode 100644 index 00000000000..8f49dd92529 --- /dev/null +++ b/spec/services/db/identity/sp_active_user_counts_spec.rb @@ -0,0 +1,72 @@ +require 'rails_helper' + +describe Db::Identity::SpActiveUserCounts do + subject { described_class } + + let(:fiscal_start_date) { 1.year.ago.strftime('%m-%d-%Y') } + let(:issuer) { 'foo' } + let(:issuer2) { 'foo2' } + + it 'is empty' do + expect(subject.call(fiscal_start_date).ntuples).to eq(0) + end + + it 'returns total active user counts per sp broken down by ial1 and ial2 for ial1 only sps' do + now = Time.zone.now + ServiceProvider.create(issuer: issuer, friendly_name: issuer) + ServiceProvider.create(issuer: issuer2, friendly_name: issuer2) + Identity.create(user_id: 1, service_provider: issuer, uuid: 'foo1', + last_ial1_authenticated_at: now) + Identity.create(user_id: 2, service_provider: issuer, uuid: 'foo2', + last_ial1_authenticated_at: now) + Identity.create(user_id: 3, service_provider: issuer2, uuid: 'foo3', + last_ial1_authenticated_at: now) + result = { issuer: issuer, total_ial1_active: 2, total_ial2_active: 0 }.to_json + result2 = { issuer: issuer2, total_ial1_active: 1, total_ial2_active: 0 }.to_json + + tuples = subject.call(fiscal_start_date) + expect(tuples.ntuples).to eq(2) + expect(tuples[0].to_json).to eq(result) + expect(tuples[1].to_json).to eq(result2) + end + + it 'returns total active user counts per sp broken down by ial1 and ial2 for ial2 only sps' do + now = Time.zone.now + ServiceProvider.create(issuer: issuer, friendly_name: issuer) + ServiceProvider.create(issuer: issuer2, friendly_name: issuer2) + Identity.create(user_id: 1, service_provider: issuer, uuid: 'foo1', + last_ial2_authenticated_at: now) + Identity.create(user_id: 2, service_provider: issuer, uuid: 'foo2', + last_ial2_authenticated_at: now) + Identity.create(user_id: 3, service_provider: issuer2, uuid: 'foo3', + last_ial2_authenticated_at: now) + result = { issuer: issuer, total_ial1_active: 0, total_ial2_active: 2 }.to_json + result2 = { issuer: issuer2, total_ial1_active: 0, total_ial2_active: 1 }.to_json + + tuples = subject.call(fiscal_start_date) + expect(tuples.ntuples).to eq(2) + expect(tuples[0].to_json).to eq(result) + expect(tuples[1].to_json).to eq(result2) + end + + it 'returns total active user counts per sp broken down by ial1 and ial2 for ial1 ial2 sps' do + now = Time.zone.now + ServiceProvider.create(issuer: issuer, friendly_name: issuer) + ServiceProvider.create(issuer: issuer2, friendly_name: issuer2) + Identity.create(user_id: 1, service_provider: issuer, uuid: 'foo1', + last_ial1_authenticated_at: now, last_ial2_authenticated_at: now) + Identity.create(user_id: 2, service_provider: issuer, uuid: 'foo2', + last_ial1_authenticated_at: now) + Identity.create(user_id: 3, service_provider: issuer2, uuid: 'foo3', + last_ial1_authenticated_at: now, last_ial2_authenticated_at: now) + Identity.create(user_id: 4, service_provider: issuer2, uuid: 'foo4', + last_ial2_authenticated_at: now) + result = { issuer: issuer, total_ial1_active: 2, total_ial2_active: 1 }.to_json + result2 = { issuer: issuer2, total_ial1_active: 1, total_ial2_active: 2 }.to_json + + tuples = subject.call(fiscal_start_date) + expect(tuples.ntuples).to eq(2) + expect(tuples[0].to_json).to eq(result) + expect(tuples[1].to_json).to eq(result2) + end +end diff --git a/spec/services/db/identity/sp_user_counts_spec.rb b/spec/services/db/identity/sp_user_counts_spec.rb index c15ae1109f5..e41171f9e34 100644 --- a/spec/services/db/identity/sp_user_counts_spec.rb +++ b/spec/services/db/identity/sp_user_counts_spec.rb @@ -4,18 +4,25 @@ subject { described_class } let(:issuer) { 'foo' } + let(:issuer2) { 'foo2' } it 'is empty' do expect(subject.call.ntuples).to eq(0) end - it 'returns the total user counts per sp' do + it 'returns the total user counts per sp broken down by ial1 and ial2' do + ServiceProvider.create(issuer: issuer, friendly_name: issuer) + ServiceProvider.create(issuer: issuer2, friendly_name: issuer2) Identity.create(user_id: 1, service_provider: issuer, uuid: 'foo1') Identity.create(user_id: 2, service_provider: issuer, uuid: 'foo2') + Identity.create(user_id: 3, service_provider: issuer, uuid: 'foo3', verified_at: Time.zone.now) + Identity.create(user_id: 4, service_provider: issuer2, uuid: 'foo4', verified_at: Time.zone.now) + result = { issuer: issuer, total: 3, ial1_total: 2, ial2_total: 1 }.to_json + result2 = { issuer: issuer2, total: 1, ial1_total: 0, ial2_total: 1 }.to_json - result = { issuer: issuer, total: 2 }.to_json - - expect(subject.call.ntuples).to eq(1) - expect(subject.call[0].to_json).to eq(result) + tuples = subject.call + expect(tuples.ntuples).to eq(2) + expect(tuples[0].to_json).to eq(result) + expect(tuples[1].to_json).to eq(result2) end end diff --git a/spec/services/db/identity/sp_user_quotas_spec.rb b/spec/services/db/identity/sp_user_quotas_spec.rb new file mode 100644 index 00000000000..bc31614f37a --- /dev/null +++ b/spec/services/db/identity/sp_user_quotas_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +describe Db::Identity::SpUserQuotas do + subject { described_class } + + let(:issuer) { 'foo' } + let(:issuer2) { 'foo2' } + let(:fiscal_start_date) { 1.year.ago.strftime('%m-%d-%Y') } + + it 'is empty' do + expect(subject.call(fiscal_start_date).ntuples).to eq(0) + end + + it 'returns the total ial2 user count per fiscal year with percent ial2 quota' do + ServiceProvider.create(issuer: issuer, friendly_name: issuer) + ServiceProvider.create(issuer: issuer2, friendly_name: issuer2, ial2_quota: 1) + Identity.create(user_id: 1, service_provider: issuer, uuid: 'foo1') + Identity.create(user_id: 2, service_provider: issuer, uuid: 'foo2') + Identity.create(user_id: 3, service_provider: issuer, uuid: 'foo3', verified_at: Time.zone.now) + Identity.create(user_id: 4, service_provider: issuer2, uuid: 'foo4', verified_at: Time.zone.now) + result = { issuer: issuer, ial2_total: 1, percent_ial2_quota: 0 }.to_json + result2 = { issuer: issuer2, ial2_total: 1, percent_ial2_quota: 100 }.to_json + + tuples = subject.call(fiscal_start_date) + expect(tuples.ntuples).to eq(2) + expect(tuples[0].to_json).to eq(result) + expect(tuples[1].to_json).to eq(result2) + end +end diff --git a/spec/services/identity_linker_spec.rb b/spec/services/identity_linker_spec.rb index fb6b1f72901..73aab5374cc 100644 --- a/spec/services/identity_linker_spec.rb +++ b/spec/services/identity_linker_spec.rb @@ -49,6 +49,30 @@ expect(last_identity.code_challenge).to eq(code_challenge) end + context 'identity.last_consented_at' do + let(:now) { Time.zone.now } + let(:six_months_ago) { 6.months.ago } + + it 'does override a previous last_consented_at by default' do + IdentityLinker.new(user, 'test.host'). + link_identity(last_consented_at: six_months_ago) + last_identity = user.reload.last_identity + expect(last_identity.last_consented_at.to_i).to eq(six_months_ago.to_i) + + IdentityLinker.new(user, 'test.host').link_identity + last_identity = user.reload.last_identity + expect(last_identity.last_consented_at.to_i).to eq(six_months_ago.to_i) + end + + it 'updates last_consented_at when present' do + IdentityLinker.new(user, 'test.host'). + link_identity(last_consented_at: now) + + last_identity = user.reload.last_identity + expect(last_identity.last_consented_at.to_i).to eq(now.to_i) + end + end + it 'rejects bad attributes names' do expect { IdentityLinker.new(user, 'test.host').link_identity(foobar: true) }. to raise_error(ArgumentError) diff --git a/spec/services/openid_connect_attribute_scoper_spec.rb b/spec/services/openid_connect_attribute_scoper_spec.rb index 872b90bdec2..fa854ae20b9 100644 --- a/spec/services/openid_connect_attribute_scoper_spec.rb +++ b/spec/services/openid_connect_attribute_scoper_spec.rb @@ -27,6 +27,7 @@ subject(:filtered) { scoper.filter(user_info) } let(:scope) { 'openid' } + let(:verified_at) { Time.zone.parse('2020-01-01').to_i } let(:user_info) do { sub: 'abcdef', @@ -46,6 +47,7 @@ postal_code: '12345', }, social_security_number: '666661234', + verified_at: verified_at, } end @@ -93,6 +95,7 @@ expect(filtered[:given_name]).to be_present expect(filtered[:family_name]).to be_present expect(filtered[:birthdate]).to be_present + expect(filtered[:verified_at]).to be_present end end @@ -103,6 +106,7 @@ expect(filtered[:given_name]).to be_present expect(filtered[:family_name]).to be_present expect(filtered[:birthdate]).to be_nil + expect(filtered[:verified_at]).to be_nil end end @@ -113,8 +117,21 @@ expect(filtered[:given_name]).to be_nil expect(filtered[:family_name]).to be_nil expect(filtered[:birthdate]).to be_present + expect(filtered[:verified_at]).to be_nil end end + + context 'with profile:verified_at scope' do + let(:scope) { 'openid profile:verified_at' } + + it 'includes the verified_at attribute' do + expect(filtered[:given_name]).to be_nil + expect(filtered[:family_name]).to be_nil + expect(filtered[:birthdate]).to be_nil + expect(filtered[:verified_at]).to eq(verified_at) + end + end + context 'with social_security_number scope' do let(:scope) { 'openid social_security_number' } @@ -131,7 +148,7 @@ let(:scope) { 'email profile' } it 'is the array of attributes corresponding to the scopes' do - expect(requested_attributes).to eq(%w[email given_name family_name birthdate]) + expect(requested_attributes).to eq(%w[email given_name family_name birthdate verified_at]) end end diff --git a/spec/services/reports/sp_active_users_report_spec.rb b/spec/services/reports/sp_active_users_report_spec.rb new file mode 100644 index 00000000000..36bdb81d95e --- /dev/null +++ b/spec/services/reports/sp_active_users_report_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +describe Reports::SpActiveUsersReport do + subject { described_class.new } + + let(:fiscal_start_date) { 1.year.ago.to_s } + let(:issuer) { 'foo' } + let(:issuer2) { 'foo2' } + + it 'is empty' do + expect(subject.call).to eq('[]') + end + + it 'returns total active user counts per sp broken down by ial1 and ial2' do + now = Time.zone.now + ServiceProvider.create(issuer: issuer, friendly_name: issuer) + Identity.create(user_id: 1, service_provider: issuer, uuid: 'foo1', + last_ial1_authenticated_at: now, last_ial2_authenticated_at: now) + Identity.create(user_id: 2, service_provider: issuer, uuid: 'foo2', + last_ial1_authenticated_at: now) + Identity.create(user_id: 3, service_provider: issuer, uuid: 'foo3', + last_ial2_authenticated_at: now) + Identity.create(user_id: 4, service_provider: issuer, uuid: 'foo4', + last_ial2_authenticated_at: now) + result = [{ issuer: issuer, total_ial1_active: 2, total_ial2_active: 3 }].to_json + + expect(subject.call).to eq(result) + end +end diff --git a/spec/services/reports/sp_user_counts_report_spec.rb b/spec/services/reports/sp_user_counts_report_spec.rb index c0a0146db1a..959c1cd02a6 100644 --- a/spec/services/reports/sp_user_counts_report_spec.rb +++ b/spec/services/reports/sp_user_counts_report_spec.rb @@ -9,10 +9,12 @@ expect(subject.call).to eq('[]') end - it 'returns the total user counts per sp' do + it 'returns the total user counts per sp broken down by ial1 and ial2' do + ServiceProvider.create(issuer: issuer, friendly_name: issuer) Identity.create(user_id: 1, service_provider: issuer, uuid: 'foo1') Identity.create(user_id: 2, service_provider: issuer, uuid: 'foo2') - result = [{ issuer: issuer, total: 2 }].to_json + Identity.create(user_id: 3, service_provider: issuer, uuid: 'foo3', verified_at: Time.zone.now) + result = [{ issuer: issuer, total: 3, ial1_total: 2, ial2_total: 1 }].to_json expect(subject.call).to eq(result) end diff --git a/spec/services/reports/sp_user_quotas_report_spec.rb b/spec/services/reports/sp_user_quotas_report_spec.rb new file mode 100644 index 00000000000..4f192c25b7c --- /dev/null +++ b/spec/services/reports/sp_user_quotas_report_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +describe Reports::SpUserQuotasReport do + subject { described_class.new } + + let(:issuer) { 'foo' } + + it 'is empty' do + expect(subject.call).to eq('[]') + end + + it 'runs correctly if the current month is before the fiscal start month of October' do + expect_report_to_run_correctly_for_fiscal_start_year_month_day(2019, 9, 1) + end + + it 'runs correctly if the current month is after the fiscal start month of October' do + expect_report_to_run_correctly_for_fiscal_start_year_month_day(2019, 11, 1) + end + + def expect_report_to_run_correctly_for_fiscal_start_year_month_day(year, month, day) + ServiceProvider.create(issuer: issuer, friendly_name: issuer) + Identity.create(user_id: 1, service_provider: issuer, uuid: 'foo1') + Identity.create(user_id: 2, service_provider: issuer, uuid: 'foo2') + Identity.create(user_id: 3, service_provider: issuer, uuid: 'foo3', verified_at: Time.zone.now) + results = [{ issuer: issuer, ial2_total: 1, percent_ial2_quota: 0 }].to_json + + Timecop.travel Date.new(year, month, day) do + expect(subject.call).to eq(results) + end + end +end diff --git a/spec/support/features/idv_from_sp_helper.rb b/spec/support/features/idv_from_sp_helper.rb index 0a0d8f9bc2e..7894e9afe77 100644 --- a/spec/support/features/idv_from_sp_helper.rb +++ b/spec/support/features/idv_from_sp_helper.rb @@ -15,13 +15,13 @@ def create_ial2_user_from_sp(email) fill_in 'Password', with: password click_continue click_acknowledge_personal_key - click_continue + click_agree_and_continue end def create_ial1_user_from_sp(email) visit_idp_from_sp_with_ial1(:oidc) register_user(email) - click_on t('forms.buttons.continue') + click_agree_and_continue end def create_ial1_user_directly(email) diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 1905947c5c3..0311b49101f 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -266,6 +266,15 @@ def click_continue click_button t('forms.buttons.continue') if page.has_button?(t('forms.buttons.continue')) end + def click_agree_and_continue + click_button t('sign_up.agree_and_continue') + end + + def click_agree_and_continue_optional + return unless page.has_button?(t('sign_up.agree_and_continue')) + click_button t('sign_up.agree_and_continue') + end + def enter_correct_otp_code_for_user(user) fill_in 'code', with: user.reload.direct_otp click_submit_default diff --git a/spec/support/features/webauthn_helper.rb b/spec/support/features/webauthn_helper.rb index 4bb56365a9c..ae5da0adb8e 100644 --- a/spec/support/features/webauthn_helper.rb +++ b/spec/support/features/webauthn_helper.rb @@ -1,13 +1,15 @@ module WebAuthnHelper def mock_webauthn_setup_challenge - allow(WebAuthn).to receive(:credential_creation_options).and_return( - challenge: webauthn_challenge.pack('c*'), + allow(WebAuthn::Credential).to receive(:options_for_create).and_return( + instance_double(WebAuthn::PublicKeyCredential::CreationOptions, + challenge: webauthn_challenge.pack('c*')), ) end def mock_webauthn_verification_challenge - allow(WebAuthn).to receive(:credential_request_options).and_return( - challenge: webauthn_challenge.pack('c*'), + allow(WebAuthn::Credential).to receive(:options_for_get).and_return( + instance_double(WebAuthn::PublicKeyCredential::RequestOptions, + challenge: webauthn_challenge.pack('c*')), ) end diff --git a/spec/support/idv_examples/confirmation_step.rb b/spec/support/idv_examples/confirmation_step.rb index 390d6c1c052..88b15481e7d 100644 --- a/spec/support/idv_examples/confirmation_step.rb +++ b/spec/support/idv_examples/confirmation_step.rb @@ -32,7 +32,7 @@ expect(page).to have_current_path(sign_up_completed_path) - click_on t('forms.buttons.continue') + click_agree_and_continue if sp == :oidc expect(current_url).to start_with('http://localhost:7654/auth/result') diff --git a/spec/support/idv_examples/max_attempts.rb b/spec/support/idv_examples/max_attempts.rb index bfacd7a82c5..030a33f033d 100644 --- a/spec/support/idv_examples/max_attempts.rb +++ b/spec/support/idv_examples/max_attempts.rb @@ -62,7 +62,7 @@ click_idv_continue complete_idv_profile_ok(user) click_acknowledge_personal_key - click_idv_continue + click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') end diff --git a/spec/support/idv_examples/sp_handoff.rb b/spec/support/idv_examples/sp_handoff.rb index c18dc086944..5aa2c54f2a5 100644 --- a/spec/support/idv_examples/sp_handoff.rb +++ b/spec/support/idv_examples/sp_handoff.rb @@ -27,7 +27,7 @@ ) expect_csp_headers_to_be_present if sp == :oidc - click_on I18n.t('forms.buttons.continue') + click_agree_and_continue expect(user.events.account_verified.size).to be(1) expect_successful_oidc_handoff if sp == :oidc @@ -57,7 +57,7 @@ ) expect_csp_headers_to_be_present if sp == :oidc - click_on I18n.t('forms.buttons.continue') + click_agree_and_continue expect(user.events.account_verified.size).to be(1) expect_successful_oidc_handoff if sp == :oidc @@ -84,7 +84,7 @@ expect_csp_headers_to_be_present if sp == :oidc - click_on I18n.t('forms.buttons.continue') + click_agree_and_continue expect_successful_oidc_handoff if sp == :oidc expect_successful_saml_handoff if sp == :saml @@ -105,7 +105,7 @@ click_idv_continue complete_idv_profile_ok(user) click_acknowledge_personal_key - click_on I18n.t('forms.buttons.continue') + click_agree_and_continue visit account_path first(:link, t('links.sign_out')).click end diff --git a/spec/support/idv_examples/sp_requested_attributes.rb b/spec/support/idv_examples/sp_requested_attributes.rb index 38b26b500f0..6b4e9c2c582 100644 --- a/spec/support/idv_examples/sp_requested_attributes.rb +++ b/spec/support/idv_examples/sp_requested_attributes.rb @@ -49,7 +49,7 @@ click_idv_continue complete_idv_profile_ok(user) click_acknowledge_personal_key - click_on I18n.t('forms.buttons.continue') + click_agree_and_continue visit account_path first(:link, t('links.sign_out')).click end diff --git a/spec/support/idv_examples/usps_otp_verification_step.rb b/spec/support/idv_examples/usps_otp_verification_step.rb index ff9c5cd250d..0b93fce2d0a 100644 --- a/spec/support/idv_examples/usps_otp_verification_step.rb +++ b/spec/support/idv_examples/usps_otp_verification_step.rb @@ -32,7 +32,7 @@ if %i[saml oidc].include?(sp) expect(current_path).to eq(sign_up_completed_path) - click_button t('forms.buttons.continue') + click_agree_and_continue if sp == :saml expect(current_url).to eq @saml_authn_request diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb index 54f14447742..90d7c656966 100644 --- a/spec/support/saml_auth_helper.rb +++ b/spec/support/saml_auth_helper.rb @@ -101,6 +101,18 @@ def sp2_saml_settings settings end + def sp_not_requesting_signed_saml_response_settings + settings = saml_settings.dup + settings.issuer = 'test_saml_sp_not_requesting_signed_response_message' + settings + end + + def sp_requesting_signed_saml_response_settings + settings = saml_settings.dup + settings.issuer = 'test_saml_sp_requesting_signed_response_message' + settings + end + def email_nameid_saml_settings_for_allowed_issuer settings = saml_settings.dup settings.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' @@ -162,6 +174,15 @@ def loa3_with_bundle_saml_settings settings end + def ial1_with_verified_at_saml_settings + settings = sp1_saml_settings + settings.authn_context = [ + settings.authn_context, + "#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}email,verified_at", + ] + settings + end + def ial1_with_bundle_saml_settings settings = sp1_saml_settings settings.authn_context = [ diff --git a/spec/support/shared_examples/account_creation.rb b/spec/support/shared_examples/account_creation.rb index 649011c6d70..281c34611bb 100644 --- a/spec/support/shared_examples/account_creation.rb +++ b/spec/support/shared_examples/account_creation.rb @@ -9,7 +9,7 @@ to(include('form-action \'self\' http://localhost:7654')) end - click_on t('forms.buttons.continue') + click_agree_and_continue expect(current_url).to eq @saml_authn_request if sp == :saml if sp == :oidc @@ -30,7 +30,7 @@ to(include('form-action \'self\' http://localhost:7654')) end - click_on t('forms.buttons.continue') + click_agree_and_continue expect(current_url).to eq @saml_authn_request if sp == :saml if sp == :oidc @@ -64,7 +64,7 @@ to(include('form-action \'self\' http://localhost:7654')) end - click_on t('forms.buttons.continue') + click_agree_and_continue expect(current_url).to eq @saml_authn_request if sp == :saml if sp == :oidc @@ -96,7 +96,7 @@ to(include('form-action \'self\' http://localhost:7654')) end - click_on t('forms.buttons.continue') + click_agree_and_continue expect(current_url).to eq @saml_authn_request if sp == :saml if sp == :oidc @@ -138,7 +138,7 @@ to(include('form-action \'self\' http://localhost:7654')) end - click_on t('forms.buttons.continue') + click_agree_and_continue expect(current_url).to eq @saml_authn_request if sp == :saml if sp == :oidc @@ -161,7 +161,7 @@ perform_in_browser(:two) do confirm_email_in_a_different_browser(first_email) - click_button t('forms.buttons.continue') + click_agree_and_continue continue_as(first_email) @@ -181,7 +181,7 @@ perform_in_browser(:two) do confirm_email_in_a_different_browser(second_email) - click_button t('forms.buttons.continue') + click_agree_and_continue continue_as(second_email) diff --git a/spec/support/shared_examples/sign_in.rb b/spec/support/shared_examples/sign_in.rb index af607367dec..3a0b3cb5254 100644 --- a/spec/support/shared_examples/sign_in.rb +++ b/spec/support/shared_examples/sign_in.rb @@ -17,7 +17,7 @@ expect(current_url).to eq(sign_up_completed_url(locale: 'es')) - click_continue + click_agree_and_continue expect(current_url).to eq @saml_authn_request if sp == :saml @@ -120,7 +120,7 @@ choose_another_security_option('personal_key') enter_personal_key(personal_key: old_personal_key) click_submit_default - click_continue + click_agree_and_continue expect(current_url).to eq @saml_authn_request if sp == :saml if sp == :oidc @@ -257,7 +257,7 @@ def ial1_sign_in_with_personal_key_goes_to_sp(sp) choose_another_security_option('personal_key') enter_personal_key(personal_key: old_personal_key) click_submit_default - click_continue + click_agree_and_continue expect(current_url).to eq @saml_authn_request if sp == :saml @@ -277,7 +277,7 @@ def ial1_sign_in_with_piv_cac_goes_to_sp(sp) click_on t('account.login.piv_cac') fill_in_piv_cac_credentials_and_submit(user) - click_continue + click_agree_and_continue return unless sp == :oidc redirect_uri = URI(current_url) diff --git a/spec/support/sp_auth_helper.rb b/spec/support/sp_auth_helper.rb index 97ed21cc85e..b64c64321db 100644 --- a/spec/support/sp_auth_helper.rb +++ b/spec/support/sp_auth_helper.rb @@ -31,7 +31,7 @@ def create_ial2_account_go_back_to_sp_and_sign_out(sp) click_continue click_acknowledge_personal_key expect(page).to have_current_path(sign_up_completed_path) - click_on t('forms.buttons.continue') + click_agree_and_continue visit sign_out_url user.reload end diff --git a/spec/view_models/sign_up_completions_show_spec.rb b/spec/view_models/sign_up_completions_show_spec.rb index 81fad42989d..41848edbcc1 100644 --- a/spec/view_models/sign_up_completions_show_spec.rb +++ b/spec/view_models/sign_up_completions_show_spec.rb @@ -5,13 +5,17 @@ @user = create(:user) end - subject do + let(:handoff) { false } + let(:consent_has_expired?) { false } + + subject(:view_model) do SignUpCompletionsShow.new( current_user: @user, ial2_requested: false, decorated_session: decorated_session, - handoff: false, + handoff: handoff, ialmax_requested: false, + consent_has_expired: consent_has_expired?, ) end @@ -27,7 +31,28 @@ describe '#service_provider_partial' do it 'returns show_sp path' do - expect(subject.service_provider_partial).to eq('sign_up/completions/show_sp') + expect(view_model.service_provider_partial).to eq('sign_up/completions/show_sp') + end + end + + describe '#heading' do + subject(:heading) { view_model.heading } + + context 'for a handoff page' do + let(:handoff) { true } + + it 'defaults to first time copy' do + expect(heading).to include(I18n.t('titles.sign_up.new_sp')) + end + + context 'when SP consent has expired' do + let(:consent_has_expired?) { true } + + it 'uses refresh copy' do + expect(heading). + to include(view_model.content_tag(:strong, I18n.t('titles.sign_up.refresh_consent'))) + end + end end end end @@ -43,25 +68,25 @@ describe '#service_provider_partial' do it 'returns show_identities path' do - expect(subject.service_provider_partial).to eq('sign_up/completions/show_identities') + expect(view_model.service_provider_partial).to eq('sign_up/completions/show_identities') end end describe '#identities' do it 'returns a users identities decorated' do identity = create_identity - expect(subject.identities).to eq([identity.decorate]) + expect(view_model.identities).to eq([identity.decorate]) end end describe '#user_has_identities?' do it 'returns true if user has identities' do create_identity - expect(subject.user_has_identities?).to eq(true) + expect(view_model.user_has_identities?).to eq(true) end it 'returns false if user has no identities' do - expect(subject.user_has_identities?).to eq(false) + expect(view_model.user_has_identities?).to eq(false) end end 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 4df9dfcb394..ec3fde46e8c 100644 --- a/spec/views/sign_up/completions/show.html.slim_spec.rb +++ b/spec/views/sign_up/completions/show.html.slim_spec.rb @@ -9,6 +9,7 @@ decorated_session: SessionDecorator.new, handoff: false, ialmax_requested: false, + consent_has_expired: false, ) end @@ -38,6 +39,7 @@ decorated_session: SessionDecorator.new, handoff: true, ialmax_requested: false, + consent_has_expired: false, ) create_identities(@user) end