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