From 1ec4ee7e7c509bf46df510c07199a81cb81d822c Mon Sep 17 00:00:00 2001 From: Adam Biagianti Date: Wed, 31 May 2017 17:07:17 -0400 Subject: [PATCH 01/17] Toggle profile back to active following reactivate **Why**: The profile won't ever be put back into an active state otherwise --- app/forms/reactivate_account_form.rb | 1 + spec/features/users/password_recovery_via_recovery_code_spec.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/forms/reactivate_account_form.rb b/app/forms/reactivate_account_form.rb index e17d3c57edc..ccde4703e62 100644 --- a/app/forms/reactivate_account_form.rb +++ b/app/forms/reactivate_account_form.rb @@ -45,6 +45,7 @@ def decrypted_pii def reencrypt_pii personal_key = password_reset_profile.encrypt_pii(user_access_key, decrypted_pii) password_reset_profile.deactivation_reason = nil + password_reset_profile.active = true password_reset_profile.save! personal_key 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 c4113a93c68..17ecdf7660d 100644 --- a/spec/features/users/password_recovery_via_recovery_code_spec.rb +++ b/spec/features/users/password_recovery_via_recovery_code_spec.rb @@ -21,6 +21,7 @@ reactivate_profile(new_password, personal_key) expect(page).to have_content t('idv.messages.personal_key') + expect(page).to have_content t('headings.account.verified_account') end scenario 'resets password, makes personal key, attempts reactivate profile', email: true do From 63566117bec62fb08c97126c5be8a741d19397fa Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Thu, 1 Jun 2017 11:07:52 -0400 Subject: [PATCH 02/17] Use OpenStruct instead of MockResponse **Why**: The Proofing::Vendor::MockResponse class doesn't do anything besides inherit from OpenStruct. --- app/services/idv/vendor_validator.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/services/idv/vendor_validator.rb b/app/services/idv/vendor_validator.rb index 9384ab8f36f..bb0ca462b9e 100644 --- a/app/services/idv/vendor_validator.rb +++ b/app/services/idv/vendor_validator.rb @@ -42,9 +42,7 @@ def agent_error_resolution(err_msg) Proofer::Resolution.new( success: false, errors: { agent: [err_msg] }, - vendor_resp: Proofer::Vendor::MockResponse.new( - reasons: [err_msg] - ) + vendor_resp: OpenStruct.new(reasons: [err_msg]) ) end end From de4befd822c39d3c0a45a8501e71d316fb60196d Mon Sep 17 00:00:00 2001 From: Adam Biagianti Date: Wed, 24 May 2017 15:55:25 -0400 Subject: [PATCH 03/17] Reanme reveal_usps_code to `flow` **Why**: The entire flow will be conditionally available, so tying the code visibility to the availability of the feature makes sense Gate usps pages, verify/phone view + specs update **Why**: Users shouldnt have the option to verify by mail if the feature is turned off, and they shouldnt be able to directly access the pages Address PR feedback **Why**: There are improvements to be made * Use Figaro config setting * Change name to better match identity_verification flag * Adds route spec * Removes checks on controller level --- app/views/verify/address/index.html.slim | 7 +- .../phone/_verification_options.html.slim | 3 + app/views/verify/phone/new.html.slim | 5 +- config/application.yml.example | 3 + config/initializers/figaro.rb | 1 + config/routes.rb | 8 ++- lib/feature_management.rb | 4 ++ .../openid_connect/openid_connect_spec.rb | 1 - spec/routing/id_verification_routing_spec.rb | 2 - spec/routing/usps_verification_routing.rb | 69 +++++++++++++++++++ 10 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 app/views/verify/phone/_verification_options.html.slim create mode 100644 spec/routing/usps_verification_routing.rb diff --git a/app/views/verify/address/index.html.slim b/app/views/verify/address/index.html.slim index 75c1d9cefd1..8044f6c6a4e 100644 --- a/app/views/verify/address/index.html.slim +++ b/app/views/verify/address/index.html.slim @@ -7,8 +7,9 @@ p .sm-col.sm-col-12.md-col-6 = link_to t('idv.buttons.activate_by_phone'), verify_phone_path, class: 'btn btn-primary mb2 center inline-block' - .sm-col.sm-col-12.md-col-6 - = link_to t('idv.buttons.activate_by_mail'), verify_usps_path, - class: 'btn btn-outline rounded-lg mb2 center' + - if FeatureManagement.enable_usps_verification? + .sm-col.sm-col-12.md-col-6 + = link_to t('idv.buttons.activate_by_mail'), verify_usps_path, + class: 'btn btn-outline rounded-lg mb2 center' = render 'shared/cancel', link: verify_cancel_path diff --git a/app/views/verify/phone/_verification_options.html.slim b/app/views/verify/phone/_verification_options.html.slim new file mode 100644 index 00000000000..8e1b6377f56 --- /dev/null +++ b/app/views/verify/phone/_verification_options.html.slim @@ -0,0 +1,3 @@ +p + = t('idv.form.no_alternate_phone_html', + link: link_to(t('idv.form.activate_by_mail'), verify_usps_path)) diff --git a/app/views/verify/phone/new.html.slim b/app/views/verify/phone/new.html.slim index 1b2a4ba7384..fc594dfa05c 100644 --- a/app/views/verify/phone/new.html.slim +++ b/app/views/verify/phone/new.html.slim @@ -26,8 +26,7 @@ em wrapper_html: { class: 'inline-block mr2' } = f.button :submit, t('forms.buttons.continue') -p - = t('idv.form.no_alternate_phone_html', - link: link_to(t('idv.form.activate_by_mail'), verify_usps_path)) +- if FeatureManagement.enable_usps_verification? + = render 'verification_options' = render @view_model.modal_partial, view_model: @view_model diff --git a/config/application.yml.example b/config/application.yml.example index 20b14d1b865..a0708c2e47f 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -58,6 +58,7 @@ development: domain_name: 'localhost:3000' enable_identity_verification: 'true' enable_test_routes: 'true' + enable_usps_verification: 'true' equifax_avs_username: 'sekret' equifax_eid_username: 'sekret' equifax_endpoint: 'sekret' @@ -109,6 +110,7 @@ production: domain_name: 'example.com' enable_identity_verification: 'false' enable_test_routes: 'false' + enable_usps_verification: 'false' equifax_avs_username: 'sekret' equifax_eid_username: 'sekret' equifax_endpoint: 'sekret' @@ -161,6 +163,7 @@ test: dashboard_api_token: '123ABC' enable_identity_verification: 'true' enable_test_routes: 'true' + enable_usps_verification: 'true' equifax_avs_username: 'sekret' equifax_eid_username: 'sekret' equifax_endpoint: 'sekret' diff --git a/config/initializers/figaro.rb b/config/initializers/figaro.rb index 77346d9a249..96d7d87a97c 100644 --- a/config/initializers/figaro.rb +++ b/config/initializers/figaro.rb @@ -6,6 +6,7 @@ 'domain_name', 'enable_identity_verification', 'enable_test_routes', + 'enable_usps_verification', 'equifax_ssh_passphrase', 'hmac_fingerprinter_key', 'idp_sso_target_url', diff --git a/config/routes.rb b/config/routes.rb index 35bf0ae568f..9a274057366 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -47,8 +47,6 @@ get '/account' => 'accounts#show' get '/account/reactivate' => 'users/reactivate_account#index', as: :reactivate_account post '/account/reactivate' => 'users/reactivate_account#create' - get '/account/verify' => 'users/verify_account#index', as: :verify_account - post '/account/verify' => 'users/verify_account#create' get '/account/verify_phone' => 'users/verify_profile_phone#index', as: :verify_profile_phone post '/account/verify_phone' => 'users/verify_profile_phone#create' @@ -135,6 +133,12 @@ put '/verify/session' => 'verify/sessions#create' delete '/verify/session' => 'verify/sessions#destroy' get '/verify/session/dupe' => 'verify/sessions#dupe' + + end + + if FeatureManagement.enable_usps_verification? + get '/account/verify' => 'users/verify_account#index', as: :verify_account + post '/account/verify' => 'users/verify_account#create' get '/verify/usps' => 'verify/usps#index' put '/verify/usps' => 'verify/usps#create' end diff --git a/lib/feature_management.rb b/lib/feature_management.rb index ff5d61cfb20..d8ef77b268d 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -48,6 +48,10 @@ def self.enable_identity_verification? Figaro.env.enable_identity_verification == 'true' end + def self.enable_usps_verification? + Figaro.env.enable_usps_verification == 'true' + end + def self.reveal_usps_code? Rails.env.development? || current_env_allowed_to_see_usps_code? end diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index 3034c1ec9b4..bd6127ede09 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -291,7 +291,6 @@ it 'prompts to finish verifying profile, then redirects to SP' do allow(FeatureManagement).to receive(:reveal_usps_code?).and_return(true) - visit oidc_auth_url sign_in_live_with_2fa(user) diff --git a/spec/routing/id_verification_routing_spec.rb b/spec/routing/id_verification_routing_spec.rb index fdbf2260f91..efe20713910 100644 --- a/spec/routing/id_verification_routing_spec.rb +++ b/spec/routing/id_verification_routing_spec.rb @@ -14,7 +14,6 @@ verify/review verify/session verify/session/dupe - verify/usps ].freeze PUT_ROUTES = %w[ @@ -22,7 +21,6 @@ verify/phone verify/review verify/session - verify/usps ].freeze DELETE_ROUTES = %w[ diff --git a/spec/routing/usps_verification_routing.rb b/spec/routing/usps_verification_routing.rb new file mode 100644 index 00000000000..88cdd574276 --- /dev/null +++ b/spec/routing/usps_verification_routing.rb @@ -0,0 +1,69 @@ +require 'rails_helper' + +describe 'USPS verification routes' do + GET_ROUTES = %w[ + account/verify + verify/usps + ].freeze + + CREATE_ROUTES = %w[ + account/verify + ].freeze + + PUT_ROUTES = %w[ + verify/usps + ].freeze + + context 'when FeatureManagement.enable_usps_verification? is false' do + before do + allow(Figaro.env).to receive(:enable_identity_verification).and_return('false') + Rails.application.reload_routes! + end + + after(:all) do + Rails.application.reload_routes! + end + + it 'does not route to endpoints controlled by feature flag' do + GET_ROUTES.each do |route| + expect(get: route). + to route_to(controller: 'pages', action: 'page_not_found', path: route) + end + + CREATE_ROUTES.each do |route| + expect(post: route). + to route_to(controller: 'pages', action: 'page_not_found', path: route) + end + + PUT_ROUTES.each do |route| + expect(put: route). + to route_to(controller: 'pages', action: 'page_not_found', path: route) + end + end + end + + context 'when FeatureManagement.enable_usps_verification? is true' do + before do + allow(Figaro.env).to receive(:enable_identity_verification).and_return('true') + Rails.application.reload_routes! + end + + after(:all) do + Rails.application.reload_routes! + end + + it 'routes to endpoints controlled by feature flag' do + GET_ROUTES.each do |route| + expect(get: route).to be_routable + end + + CREATE_ROUTES.each do |route| + expect(post: route).to be_routable + end + + PUT_ROUTES.each do |route| + expect(put: route).to be_routable + end + end + end +end From 598cba5b45806a7c35ceb01b2edd11d018ad5256 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Thu, 1 Jun 2017 15:41:51 -0400 Subject: [PATCH 04/17] Check in YML-XML conversion scripts **Why**: Share the scripts we use to share our files with vendors --- lib/i18n_converter.rb | 39 ++++++++++++++++ scripts/i18n-xml-to-yml | 5 ++ scripts/i18n-yml-to-xml | 5 ++ spec/lib/i18n_converter_spec.rb | 81 +++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 lib/i18n_converter.rb create mode 100755 scripts/i18n-xml-to-yml create mode 100755 scripts/i18n-yml-to-xml create mode 100644 spec/lib/i18n_converter_spec.rb diff --git a/lib/i18n_converter.rb b/lib/i18n_converter.rb new file mode 100644 index 00000000000..9c5514598fd --- /dev/null +++ b/lib/i18n_converter.rb @@ -0,0 +1,39 @@ +require 'yaml' +require 'active_support/core_ext/hash/conversions' + +class I18nConverter + def initialize(stdin:, stdout:) + @stdin = stdin + @stdout = stdout + end + + def xml_to_yml + return if bad_usage?(in_format: :xml, out_format: :yml) + + data = Hash.from_xml(stdin.read) + data_hash = data['hash'] + data = data_hash if data_hash + stdout.puts YAML.dump(data) + end + + def yml_to_xml + return if bad_usage?(in_format: :yml, out_format: :xml) + + data = YAML.safe_load(stdin.read) + stdout.puts data.to_xml + end + + private + + attr_reader :stdin, :stdout + + def bad_usage?(in_format:, out_format:) + return false unless stdin.tty? + + stdout.puts "Usage: cat en.#{in_format} | #{$PROGRAM_NAME} > output.#{out_format}" + # rubocop:disable Rails/Exit + exit 1 + # rubocop:enable Rails/Exit + true + end +end diff --git a/scripts/i18n-xml-to-yml b/scripts/i18n-xml-to-yml new file mode 100755 index 00000000000..81ee85559f7 --- /dev/null +++ b/scripts/i18n-xml-to-yml @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.expand_path('../lib', File.dirname(__FILE__))) +require 'i18n_converter' + +I18nConverter.new(stdin: STDIN, stdout: STDOUT).xml_to_yml diff --git a/scripts/i18n-yml-to-xml b/scripts/i18n-yml-to-xml new file mode 100755 index 00000000000..1b78bcb06ac --- /dev/null +++ b/scripts/i18n-yml-to-xml @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.expand_path('../lib', File.dirname(__FILE__))) +require 'i18n_converter' + +I18nConverter.new(stdin: STDIN, stdout: STDOUT).yml_to_xml diff --git a/spec/lib/i18n_converter_spec.rb b/spec/lib/i18n_converter_spec.rb new file mode 100644 index 00000000000..5448449019b --- /dev/null +++ b/spec/lib/i18n_converter_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' +require 'i18n_converter' + +RSpec.describe I18nConverter do + let(:translation_yml) do + <<~YAML + --- + en: + test: + key: Some string + other_test: + other_key: Other key + some_key: Some key + YAML + end + + let(:translation_xml) do + <<~XML + + + + + Some string + + + Other key + Some key + + + + XML + end + + subject(:converter) { I18nConverter.new(stdin: stdin, stdout: stdout) } + + describe '.yml_to_xml' do + let(:stdout) { StringIO.new } + let(:stdin) { StringIO.new(translation_yml) } + + context 'with a TTY on STDIN' do + let(:stdin) { instance_double('IO', tty?: true) } + + it 'prints an error and exits' do + expect(converter).to receive(:exit).with(1) + + converter.yml_to_xml + + expect(stdout.string.chomp).to eq("Usage: cat en.yml | #{$PROGRAM_NAME} > output.xml") + end + end + + it 'outputs XML' do + converter.yml_to_xml + + expect(stdout.string).to eq(translation_xml) + end + end + + describe '.xml_to_yml' do + let(:stdout) { StringIO.new } + let(:stdin) { StringIO.new(translation_xml) } + + context 'with a TTY on STDIN' do + let(:stdin) { instance_double('IO', tty?: true) } + + it 'prints an error and exits' do + expect(converter).to receive(:exit).with(1) + + converter.xml_to_yml + + expect(stdout.string.chomp).to eq("Usage: cat en.xml | #{$PROGRAM_NAME} > output.yml") + end + end + + it 'outputs YML' do + converter.xml_to_yml + + expect(stdout.string).to eq(translation_yml) + end + end +end From 9343ac3dafc2cef286d71297f300dfaa6b003382 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Thu, 1 Jun 2017 09:15:36 -0400 Subject: [PATCH 05/17] Support multiple redirect_uri values in OIDC **Why**: Makes it easier to manage OIDC Service Providers --- .../service_provider_session_decorator.rb | 2 +- app/forms/openid_connect_authorize_form.rb | 2 +- app/models/null_service_provider.rb | 4 +- app/models/service_provider.rb | 4 ++ app/services/openid_connect_redirector.rb | 16 ++++--- config/service_providers.yml | 24 +++++++---- ...iple_redirect_uris_to_service_providers.rb | 5 +++ db/schema.rb | 3 +- spec/models/null_service_provider_spec.rb | 6 +-- spec/models/service_provider_spec.rb | 25 +++++++++++ .../openid_connect_redirector_spec.rb | 42 +++++++++++++++++++ .../services/service_provider_updater_spec.rb | 16 +++++++ 12 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 db/migrate/20170531204549_add_multiple_redirect_uris_to_service_providers.rb diff --git a/app/decorators/service_provider_session_decorator.rb b/app/decorators/service_provider_session_decorator.rb index f9ceb8b90b8..47a86c2f6f6 100644 --- a/app/decorators/service_provider_session_decorator.rb +++ b/app/decorators/service_provider_session_decorator.rb @@ -58,7 +58,7 @@ def sp_name end def sp_return_url - if sp.redirect_uri.present? && openid_connect_redirector.valid? + if sp.redirect_uris.present? && openid_connect_redirector.valid? openid_connect_redirector.decline_redirect_uri else sp.return_to_sp_url diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index 800c53cb4aa..c5723c0eb84 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -61,7 +61,7 @@ def loa3_requested? end def sp_redirect_uri - service_provider.redirect_uri + openid_connect_redirector.validated_input_redirect_uri end def service_provider diff --git a/app/models/null_service_provider.rb b/app/models/null_service_provider.rb index 19ca25a283b..2998c226224 100644 --- a/app/models/null_service_provider.rb +++ b/app/models/null_service_provider.rb @@ -31,5 +31,7 @@ def friendly_name; end def return_to_sp_url; end - def redirect_uri; end + def redirect_uris + [] + end end diff --git a/app/models/service_provider.rb b/app/models/service_provider.rb index d4335310121..e31d6f1e223 100644 --- a/app/models/service_provider.rb +++ b/app/models/service_provider.rb @@ -43,4 +43,8 @@ def encryption_opts def live? active? && approved? end + + def redirect_uris + super.presence || Array(redirect_uri) + end end diff --git a/app/services/openid_connect_redirector.rb b/app/services/openid_connect_redirector.rb index 9a3978b6674..c960eb68a7d 100644 --- a/app/services/openid_connect_redirector.rb +++ b/app/services/openid_connect_redirector.rb @@ -54,6 +54,10 @@ def logout_redirect_uri URIService.add_params(validated_input_redirect_uri, state: state) end + def validated_input_redirect_uri + redirect_uri if redirect_uri_matches_sp_redirect_uri? + end + private attr_reader :redirect_uri, :service_provider, :state, :errors, :error_attr @@ -72,14 +76,8 @@ def validate_redirect_uri_matches_sp_redirect_uri def redirect_uri_matches_sp_redirect_uri? redirect_uri.present? && service_provider.active? && - redirect_uri.start_with?(sp_redirect_uri) - end - - def validated_input_redirect_uri - redirect_uri if redirect_uri_matches_sp_redirect_uri? - end - - def sp_redirect_uri - service_provider.redirect_uri + service_provider.redirect_uris.any? do |sp_redirect_uri| + redirect_uri.start_with?(sp_redirect_uri) + end end end diff --git a/config/service_providers.yml b/config/service_providers.yml index 62738690854..bdc1760c0af 100644 --- a/config/service_providers.yml +++ b/config/service_providers.yml @@ -40,14 +40,16 @@ test: sp_initiated_login_url: 'http://test.host/test/saml' 'urn:gov:gsa:openidconnect:test': - redirect_uri: 'gov.gsa.openidconnect.test://result' + redirect_uris: + - 'gov.gsa.openidconnect.test://result' cert: 'saml_test_sp' friendly_name: 'Example iOS App' agency: '18F' logo: 'generic.svg' 'urn:gov:gsa:openidconnect:sp:server': - redirect_uri: 'http://localhost:7654/' + redirect_uris: + - 'http://localhost:7654/' cert: 'saml_test_sp' friendly_name: 'Test SP' @@ -112,13 +114,15 @@ development: - email 'urn:gov:gsa:openidconnect:development': - redirect_uri: 'gov.gsa.openidconnect.development://result' + redirect_uris: + - 'gov.gsa.openidconnect.development://result' friendly_name: 'Example iOS App' agency: '18F' logo: 'generic.svg' 'urn:gov:gsa:openidconnect:sp:sinatra': - redirect_uri: 'http://localhost:9292/' + redirect_uris: + - 'http://localhost:9292/' cert: 'sp_sinatra_demo' friendly_name: 'Example Sinatra App' @@ -306,26 +310,30 @@ production: # CBP Jobs 'urn:gov:dhs.cbp.jobs:openidconnect:cert': - redirect_uri: 'https://careers-cert.cbp.dhs.gov/hrm/app' + redirect_uris: + - 'https://careers-cert.cbp.dhs.gov/hrm/app' friendly_name: 'CBP Jobs' agency: 'DHS' logo: 'cbp.png' 'urn:gov:dhs.cbp.jobs:openidconnect:cert:app': - redirect_uri: 'gov.dhs.cbp.jobs.applicant.cert://result' + redirect_uris: + - 'gov.dhs.cbp.jobs.applicant.cert://result' friendly_name: 'CBP Jobs' agency: 'DHS' logo: 'cbp.png' 'urn:gov:dhs.cbp.jobs:openidconnect:prod': - redirect_uri: 'https://careers.cbp.dhs.gov/hrm/app' + redirect_uris: + - 'https://careers.cbp.dhs.gov/hrm/app' friendly_name: 'CBP Jobs' agency: 'DHS' logo: 'cbp.png' allow_on_prod_chef_env: 'true' 'urn:gov:dhs.cbp.jobs:openidconnect:prod:app': - redirect_uri: 'gov.dhs.cbp.jobs.applicant://result' + redirect_uris: + - 'gov.dhs.cbp.jobs.applicant://result' friendly_name: 'CBP Jobs' agency: 'DHS' logo: 'cbp.png' diff --git a/db/migrate/20170531204549_add_multiple_redirect_uris_to_service_providers.rb b/db/migrate/20170531204549_add_multiple_redirect_uris_to_service_providers.rb new file mode 100644 index 00000000000..6d3bdd073a8 --- /dev/null +++ b/db/migrate/20170531204549_add_multiple_redirect_uris_to_service_providers.rb @@ -0,0 +1,5 @@ +class AddMultipleRedirectUrisToServiceProviders < ActiveRecord::Migration + def change + add_column :service_providers, :redirect_uris, :string, array: true, default: [] + end +end diff --git a/db/schema.rb b/db/schema.rb index 1d03ea371ee..4895d88e652 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170512214024) do +ActiveRecord::Schema.define(version: 20170531204549) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -123,6 +123,7 @@ t.boolean "active", default: false, null: false t.boolean "approved", default: false, null: false t.boolean "native", default: false, null: false + t.string "redirect_uris", default: [], array: true end add_index "service_providers", ["issuer"], name: "index_service_providers_on_issuer", unique: true, using: :btree diff --git a/spec/models/null_service_provider_spec.rb b/spec/models/null_service_provider_spec.rb index b822be348fd..7035562b026 100644 --- a/spec/models/null_service_provider_spec.rb +++ b/spec/models/null_service_provider_spec.rb @@ -64,9 +64,9 @@ end end - describe '#redirect_uri' do - it 'returns nil' do - expect(subject.redirect_uri).to be_nil + describe '#redirect_uris' do + it 'returns empty array' do + expect(subject.redirect_uris).to eq([]) end end end diff --git a/spec/models/service_provider_spec.rb b/spec/models/service_provider_spec.rb index 75e0f4a7339..3bff59af8fb 100644 --- a/spec/models/service_provider_spec.rb +++ b/spec/models/service_provider_spec.rb @@ -130,4 +130,29 @@ end end end + + describe '#redirect_uris' do + context 'when a legacy single redirect_uri is set but not redirect_uris' do + let(:service_provider) do + ServiceProvider.new( + redirect_uri: 'http://a.example.com', + redirect_uris: [] + ) + end + + it 'is an array of the legacy redirect_uri' do + expect(service_provider.redirect_uris).to eq(%w[http://a.example.com]) + end + end + + context 'when there are new-style multiple redirect_uris' do + let(:service_provider) do + ServiceProvider.new(redirect_uris: %w[http://b.example.com my-app://result]) + end + + it 'is the new-style multiple redirect_uris' do + expect(service_provider.redirect_uris).to eq(%w[http://b.example.com my-app://result]) + end + end + end end diff --git a/spec/services/openid_connect_redirector_spec.rb b/spec/services/openid_connect_redirector_spec.rb index 031cc686ea1..a3751f3b857 100644 --- a/spec/services/openid_connect_redirector_spec.rb +++ b/spec/services/openid_connect_redirector_spec.rb @@ -95,4 +95,46 @@ to eq(URIService.add_params(redirect_uri, state: state)) end end + + describe '#validated_input_redirect_uri' do + let(:service_provider) { ServiceProvider.new(redirect_uris: redirect_uris, active: true) } + + subject(:validated_input_redirect_uri) { redirector.validated_input_redirect_uri } + + context 'when the service provider has no redirect URIs' do + let(:redirect_uris) { [] } + + it 'is nil' do + expect(validated_input_redirect_uri).to be_nil + end + end + + context 'when the service provider has 2 redirect URIs' do + let(:redirect_uris) { %w[http://localhost:1234/result my-app://result] } + + context 'when a URL matching the first redirect_uri is passed in' do + let(:redirect_uri) { 'http://localhost:1234/result/more' } + + it 'is that URL' do + expect(validated_input_redirect_uri).to eq(redirect_uri) + end + end + + context 'when a URL matching the second redirect_uri is passed in' do + let(:redirect_uri) { 'my-app://result/more' } + + it 'is that URL' do + expect(validated_input_redirect_uri).to eq(redirect_uri) + end + end + + context 'when a URL matching the neither redirect_uri is passed in' do + let(:redirect_uri) { 'https://example.com' } + + it 'is nil' do + expect(validated_input_redirect_uri).to be_nil + end + end + end + end end diff --git a/spec/services/service_provider_updater_spec.rb b/spec/services/service_provider_updater_spec.rb index f10ab93cdc4..8f62726fe72 100644 --- a/spec/services/service_provider_updater_spec.rb +++ b/spec/services/service_provider_updater_spec.rb @@ -6,6 +6,8 @@ let(:fake_dashboard_url) { 'http://dashboard.example.org' } let(:dashboard_sp_issuer) { 'some-dashboard-service-provider' } let(:inactive_dashboard_sp_issuer) { 'old-dashboard-service-provider' } + let(:openid_connect_issuer) { 'sp:test:foo:bar' } + let(:openid_connect_redirect_uris) { %w[http://localhost:1234 my-app:/result] } let(:dashboard_service_providers) do [ { @@ -43,6 +45,12 @@ acs_url: 'http://nasty-override.example.org/saml/login', active: true, }, + { + issuer: openid_connect_issuer, + agency: 'a service provider', + redirect_uris: openid_connect_redirect_uris, + active: true, + }, ] end @@ -115,6 +123,14 @@ expect(sp.agency).to_not eq 'trying to override a test SP' end + + it 'updates redirect_uris' do + subject.run + + sp = ServiceProvider.from_issuer(openid_connect_issuer) + + expect(sp.redirect_uris).to eq(openid_connect_redirect_uris) + end end context 'dashboard is not available' do From 25bc68bddd921b8e368a182eb452bd31fe162c83 Mon Sep 17 00:00:00 2001 From: Brian Hurst Date: Fri, 2 Jun 2017 12:35:59 -0400 Subject: [PATCH 06/17] Update cancellation flash message --- config/locales/sign_up/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/sign_up/en.yml b/config/locales/sign_up/en.yml index 7aa863b3806..531a247d59f 100644 --- a/config/locales/sign_up/en.yml +++ b/config/locales/sign_up/en.yml @@ -7,7 +7,7 @@ cancel: modal_header: Are you sure you want to cancel? warning_header: If you cancel now - success: The record of your information has been cleared + success: Your account has been deleted. We did not save your information. warning_points: - You won’t have a login.gov account - We won't keep a record of your email address, password, and phone number From 232eda5c358a21e3b531ded6a4b2a052e6298b0d Mon Sep 17 00:00:00 2001 From: Adam Biagianti Date: Fri, 2 Jun 2017 13:18:46 -0400 Subject: [PATCH 07/17] Add controller/view/xlations/specs for recovery **Why**: We are changing around our accout recovery flow to better match the existing experience --- app/assets/stylesheets/components/_util.scss | 11 ++++ .../concerns/two_factor_authenticatable.rb | 2 +- .../reactivate_account_controller.rb | 20 +++++++ app/controllers/verify_controller.rb | 10 ++++ app/views/reactivate_account/index.html.slim | 22 ++++++++ .../users/reactivate_account/index.html.slim | 2 +- config/locales/headings/en.yml | 1 + config/locales/headings/es.yml | 1 + config/locales/instructions/en.yml | 12 ++++- config/locales/instructions/es.yml | 6 +++ config/locales/links/en.yml | 4 ++ config/locales/links/es.yml | 4 ++ config/locales/titles/en.yml | 2 +- config/locales/titles/es.yml | 2 +- config/routes.rb | 2 + .../reactivate_account_controller_spec.rb | 54 +++++++++++++++++++ spec/controllers/verify_controller_spec.rb | 10 ++++ ...assword_recovery_via_recovery_code_spec.rb | 6 ++- spec/features/users/user_profile_spec.rb | 3 +- 19 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 app/controllers/reactivate_account_controller.rb create mode 100644 app/views/reactivate_account/index.html.slim create mode 100644 spec/controllers/reactivate_account_controller_spec.rb diff --git a/app/assets/stylesheets/components/_util.scss b/app/assets/stylesheets/components/_util.scss index 3e52e2c8bb4..26b80beb1f0 100644 --- a/app/assets/stylesheets/components/_util.scss +++ b/app/assets/stylesheets/components/_util.scss @@ -19,6 +19,17 @@ vertical-align: middle; } + +.block-center { + margin: 0 auto; +} + +.scale-down { + // trigger anti-aliasing in chrome + backface-visibility: hidden; + transform: scale(.7); +} + @media #{$breakpoint-sm} { // scss-lint:disable ImportantRule .sm-display-inline-block { display: inline-block !important; } diff --git a/app/controllers/concerns/two_factor_authenticatable.rb b/app/controllers/concerns/two_factor_authenticatable.rb index 24fd78f0851..73f0761a1a2 100644 --- a/app/controllers/concerns/two_factor_authenticatable.rb +++ b/app/controllers/concerns/two_factor_authenticatable.rb @@ -186,7 +186,7 @@ def after_otp_action_path elsif @updating_existing_number account_path elsif decorated_user.password_reset_profile.present? - reactivate_account_path + manage_reactivate_account_path else account_path end diff --git a/app/controllers/reactivate_account_controller.rb b/app/controllers/reactivate_account_controller.rb new file mode 100644 index 00000000000..c8e43c8b85f --- /dev/null +++ b/app/controllers/reactivate_account_controller.rb @@ -0,0 +1,20 @@ +class ReactivateAccountController < ApplicationController + before_action :confirm_two_factor_authenticated + before_action :confirm_password_reset_profile + + def index + user_session[:acknowledge_personal_key] ||= true + end + + def update + user_session.delete(:acknowledge_personal_key) + redirect_to verify_url + end + + protected + + def confirm_password_reset_profile + return if current_user.decorate.password_reset_profile + redirect_to root_url + end +end diff --git a/app/controllers/verify_controller.rb b/app/controllers/verify_controller.rb index d02d8738cca..aa9452e8c13 100644 --- a/app/controllers/verify_controller.rb +++ b/app/controllers/verify_controller.rb @@ -3,6 +3,7 @@ class VerifyController < ApplicationController before_action :confirm_two_factor_authenticated before_action :confirm_idv_needed, only: %i[cancel fail] + before_action :profile_needs_reactivation?, only: [:index] def index if active_profile? @@ -26,6 +27,15 @@ def fail private + def profile_needs_reactivation? + return unless password_reset_profile && user_session[:acknowledge_personal_key] == true + redirect_to manage_reactivate_account_url + end + + def password_reset_profile + current_user.decorate.password_reset_profile + end + def active_profile? current_user.active_profile.present? end diff --git a/app/views/reactivate_account/index.html.slim b/app/views/reactivate_account/index.html.slim new file mode 100644 index 00000000000..845f0f0b6aa --- /dev/null +++ b/app/views/reactivate_account/index.html.slim @@ -0,0 +1,22 @@ +- title t('titles.reactivate_account') + +h1.h3.mt0.mb2 = t('headings.account.reactivate') + +p.mb4 = t('instructions.account.reactivate.intro') + +h2.h4.pb1.border-bottom = t('instructions.account.reactivate.begin') + +h3.fs-20p.sans-serif.mt4 = t('instructions.account.reactivate.with_key') + +p.mb0 = t('instructions.account.reactivate.explanation') + +.scale-down + = render 'partials/personal_key/key', code: 'XXXX-XXXX-XXXX-XXXX' + +.block-center.center.col-10 + .col-12.mb2 + = link_to t('links.account.reactivate.with_key'), reactivate_account_path, + class: 'btn btn-primary block' + = form_tag manage_reactivate_account_path, method: :put, class: 'col-12' do + = button_tag t('links.account.reactivate.without_key'), type: 'submit', + class: 'btn btn-secondary block col-12' diff --git a/app/views/users/reactivate_account/index.html.slim b/app/views/users/reactivate_account/index.html.slim index 555995a36f6..d56954d0e6d 100644 --- a/app/views/users/reactivate_account/index.html.slim +++ b/app/views/users/reactivate_account/index.html.slim @@ -1,4 +1,4 @@ -- title t('titles.reactivate_profile') +- title t('titles.reactivate_account') h1.h3.my0 = t('forms.reactivate_profile.title') p.mt-tiny.mb0 = t('forms.reactivate_profile.instructions') diff --git a/config/locales/headings/en.yml b/config/locales/headings/en.yml index 9c922b03e5e..d1cf87dadb9 100644 --- a/config/locales/headings/en.yml +++ b/config/locales/headings/en.yml @@ -18,6 +18,7 @@ en: account_history: Account history login_info: Your account profile_info: Profile information + reactivate: Reactivate your account two_factor: Two-factor authentication verified_account: Verified Account personal_key: Here is your personal key diff --git a/config/locales/headings/es.yml b/config/locales/headings/es.yml index ebdd31c67ca..dd431a09d86 100644 --- a/config/locales/headings/es.yml +++ b/config/locales/headings/es.yml @@ -18,6 +18,7 @@ es: account_history: La historia de la cuenta login_info: Información de la cuenta profile_info: NOT TRANSLATED YET + reactivate: NOT TRANSLATED YET two_factor: Autenticación de dos factores verified_account: Cuenta verificada personal_key: Asegúrese de que siempre puede iniciar sesión diff --git a/config/locales/instructions/en.yml b/config/locales/instructions/en.yml index e70b128185d..320ba4a18fa 100644 --- a/config/locales/instructions/en.yml +++ b/config/locales/instructions/en.yml @@ -17,6 +17,16 @@ en: %{resend_code_link} fallback_html: If you can't take a phone call right now, you can %{link} wrong_number_html: Entered the wrong phone number? %{link} + account: + reactivate: + begin: Let's get started. + explanation: > + When you created your account, we gave you a list of words and asked + you to store them in a safe place. It looked similar to this: + intro: > + We take extra steps to keep your personal information secure and private, + so resetting your password takes a little extra effort. + with_key: Do you have your personal key? forgot_password: close_window: You can close this browser window once you have reset your password. password: @@ -28,7 +38,7 @@ en: phrases. Also avoid repeating passwords from other online accounts such as banks, email and social media. info: - lead: > + lead: > It must be at least %{min_length} characters long and not be a commonly used password. That's it! strength: diff --git a/config/locales/instructions/es.yml b/config/locales/instructions/es.yml index 93937aff8e2..89798cec450 100644 --- a/config/locales/instructions/es.yml +++ b/config/locales/instructions/es.yml @@ -15,6 +15,12 @@ es: confirm_code_html: NOT TRANSLATED YET fallback_html: NOT TRANSLATED YET wrong_number_html: ¿Ha ingresado el número de teléfono equivocado? %{link} + account: + reactivate: + begin: NOT TRANSLATED YET + explanation: NOT TRANSLATED YET + intro: NOT TRANSLATED YET + with_key: NOT TRANSLATED YET forgot_password: close_window: Puede cerrar esta ventana del navegador despues que haya restablecido su contraseña. password: diff --git a/config/locales/links/en.yml b/config/locales/links/en.yml index 478b0a5eb85..be0a5af5ebc 100644 --- a/config/locales/links/en.yml +++ b/config/locales/links/en.yml @@ -1,6 +1,10 @@ --- en: links: + account: + reactivate: + with_key: I have my key + without_key: I don't have my key back_to_sp: Back to %{sp} contact: Contact copy: Copy diff --git a/config/locales/links/es.yml b/config/locales/links/es.yml index 9838dabd55a..4e6f377c099 100644 --- a/config/locales/links/es.yml +++ b/config/locales/links/es.yml @@ -1,6 +1,10 @@ --- es: links: + account: + reactivate: + with_key: NOT TRANSLATED YET + without_key: NOT TRANSLATED YET back_to_sp: "Volver a %{sp}" contact: Contactar copy: NOT TRANSLATED YET diff --git a/config/locales/titles/en.yml b/config/locales/titles/en.yml index d591702a414..e723b867945 100644 --- a/config/locales/titles/en.yml +++ b/config/locales/titles/en.yml @@ -15,7 +15,7 @@ en: confirm: Confirm the password for your account forgot: Reset the password for your account account: Account - reactivate_profile: Reactivate profile + reactivate_account: Reactivate your account personal_key: Just in case registrations: new: Sign up for a account diff --git a/config/locales/titles/es.yml b/config/locales/titles/es.yml index a40ced78c7f..3808dc22116 100644 --- a/config/locales/titles/es.yml +++ b/config/locales/titles/es.yml @@ -15,7 +15,7 @@ es: confirm: NOT TRANSLATED YET forgot: NOT TRANSLATED YET account: NOT TRANSLATED YET - reactivate_profile: NOT TRANSLATED YET + reactivate_account: NOT TRANSLATED YET personal_key: NOT TRANSLATED YET registrations: new: NOT TRANSLATED YET diff --git a/config/routes.rb b/config/routes.rb index 2feecce8ef1..9f4ab0e068a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -47,6 +47,8 @@ get '/account' => 'accounts#show' get '/account/reactivate' => 'users/reactivate_account#index', as: :reactivate_account post '/account/reactivate' => 'users/reactivate_account#create' + get '/account/reactivate/start' => 'reactivate_account#index', as: :manage_reactivate_account + put '/account/reactivate/start' => 'reactivate_account#update' get '/account/verify_phone' => 'users/verify_profile_phone#index', as: :verify_profile_phone post '/account/verify_phone' => 'users/verify_profile_phone#create' diff --git a/spec/controllers/reactivate_account_controller_spec.rb b/spec/controllers/reactivate_account_controller_spec.rb new file mode 100644 index 00000000000..b88d9593cb2 --- /dev/null +++ b/spec/controllers/reactivate_account_controller_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +describe ReactivateAccountController do + let(:user) { create(:user, profiles: profiles) } + let(:profiles) { [] } + + before { stub_sign_in(user) } + + describe 'before_actions' do + it 'requires the user to be logged in' do + expect(subject).to have_actions( + :confirm_two_factor_authenticated + ) + end + end + + describe '#index' do + context 'with a password reset profile' do + let(:profiles) { [create(:profile, deactivation_reason: :password_reset)] } + + it 'renders the index template' do + get :index + + expect(subject).to render_template(:index) + end + + it 'sets a key on the user session for future redirect guidance' do + get :index + + expect(subject.user_session[:acknowledge_personal_key]).to eq true + end + end + + context 'wthout a password reset profile' do + let(:profiles) { [create(:profile, :active)] } + it 'redirects to the root url' do + get :index + + expect(response).to redirect_to root_url + end + end + end + + describe '#update' do + let(:profiles) { [create(:profile, deactivation_reason: :password_reset)] } + + it 'redirects user to verify_url' do + put :update + + expect(subject.user_session[:acknowledge_personal_key]).to be_nil + expect(response).to redirect_to verify_url + end + end +end diff --git a/spec/controllers/verify_controller_spec.rb b/spec/controllers/verify_controller_spec.rb index 05119f85df6..ca51bf85787 100644 --- a/spec/controllers/verify_controller_spec.rb +++ b/spec/controllers/verify_controller_spec.rb @@ -34,6 +34,16 @@ expect(response).to redirect_to verify_fail_url end + + it 'redirects to account recovery if user has a password reset profile' do + profile = create(:profile, deactivation_reason: :password_reset) + stub_sign_in(profile.user) + allow(subject).to receive(:user_session).and_return(acknowledge_personal_key: true) + + get :index + + expect(response).to redirect_to manage_reactivate_account_url + end end describe '#activated' do 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 17ecdf7660d..b6f33f6adb6 100644 --- a/spec/features/users/password_recovery_via_recovery_code_spec.rb +++ b/spec/features/users/password_recovery_via_recovery_code_spec.rb @@ -16,6 +16,10 @@ click_submit_default enter_correct_otp_code_for_user(user) + expect(current_path).to eq manage_reactivate_account_path + + click_on t('links.account.reactivate.with_key') + expect(current_path).to eq reactivate_account_path reactivate_profile(new_password, personal_key) @@ -33,8 +37,6 @@ click_submit_default enter_correct_otp_code_for_user(user) - expect(current_path).to eq reactivate_account_path - visit manage_personal_key_path new_personal_key = scrape_personal_key diff --git a/spec/features/users/user_profile_spec.rb b/spec/features/users/user_profile_spec.rb index 680a6444f9c..e04ded54ba7 100644 --- a/spec/features/users/user_profile_spec.rb +++ b/spec/features/users/user_profile_spec.rb @@ -74,8 +74,7 @@ reset_password_and_sign_back_in(user, user_password) click_submit_default enter_correct_otp_code_for_user(user) - click_on t('links.cancel') - click_on t('account.index.reactivation.reverify') + click_on t('links.account.reactivate.without_key') click_idv_begin complete_idv_profile_ok(user) click_acknowledge_personal_key From f3d4501081303b75c451d5a0bfae1d492f2f1b4f Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Fri, 2 Jun 2017 14:47:26 -0400 Subject: [PATCH 08/17] Redirect to branded page after canceling **Why**: If you came from an SP, canceling account creation should take you back to the branded start page. --- app/controllers/users_controller.rb | 3 ++- .../service_provider_session_decorator.rb | 4 ++-- app/decorators/session_decorator.rb | 4 ++-- app/views/sign_up/registrations/new.html.slim | 2 +- spec/controllers/users_controller_spec.rb | 9 +++++++++ .../service_provider_session_decorator_spec.rb | 6 +++--- spec/decorators/session_decorator_spec.rb | 6 +++--- spec/features/saml/loa1/account_creation_spec.rb | 15 +++++++++++++++ .../sign_up/registrations/new.html.slim_spec.rb | 4 ++-- 9 files changed, 39 insertions(+), 14 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 80285bd19c7..e954cbde8cf 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,8 +1,9 @@ class UsersController < ApplicationController def destroy + path_after_cancellation = decorated_session.cancel_link_path destroy_user flash[:success] = t('sign_up.cancel.success') - redirect_to root_path + redirect_to path_after_cancellation end private diff --git a/app/decorators/service_provider_session_decorator.rb b/app/decorators/service_provider_session_decorator.rb index f9ceb8b90b8..a20521ebd48 100644 --- a/app/decorators/service_provider_session_decorator.rb +++ b/app/decorators/service_provider_session_decorator.rb @@ -65,8 +65,8 @@ def sp_return_url end end - def cancel_link_url - sign_up_start_url(request_id: sp_session[:request_id]) + def cancel_link_path + sign_up_start_path(request_id: sp_session[:request_id]) end private diff --git a/app/decorators/session_decorator.rb b/app/decorators/session_decorator.rb index e4213d58c55..6702f444407 100644 --- a/app/decorators/session_decorator.rb +++ b/app/decorators/session_decorator.rb @@ -37,7 +37,7 @@ def sp_return_url; end def requested_attributes; end - def cancel_link_url - root_url + def cancel_link_path + root_path end end diff --git a/app/views/sign_up/registrations/new.html.slim b/app/views/sign_up/registrations/new.html.slim index 19357695b27..281272e631a 100644 --- a/app/views/sign_up/registrations/new.html.slim +++ b/app/views/sign_up/registrations/new.html.slim @@ -13,4 +13,4 @@ p.mt-tiny.mb0#email-description = t('instructions.registration.email') = f.button :submit, t('forms.buttons.submit.default'), class: 'btn-wide mb4' -= render 'shared/cancel', link: decorated_session.cancel_link_url += render 'shared/cancel', link: decorated_session.cancel_link_path diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index ba5adfcb8e7..abfa6fd336b 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -25,5 +25,14 @@ expect { delete :destroy }.to change(User, :count).by(-1) end + + it 'redirects to the branded start page if the user came from an SP' do + session[:sp] = { issuer: 'http://localhost:3000', request_id: 'foo' } + + delete :destroy + + expect(response). + to redirect_to sign_up_start_path(request_id: 'foo') + end end end diff --git a/spec/decorators/service_provider_session_decorator_spec.rb b/spec/decorators/service_provider_session_decorator_spec.rb index 324e7fb1f13..3f1ed39ff0c 100644 --- a/spec/decorators/service_provider_session_decorator_spec.rb +++ b/spec/decorators/service_provider_session_decorator_spec.rb @@ -93,14 +93,14 @@ end end - describe '#cancel_link_url' do + describe '#cancel_link_path' do it 'returns sign_up_start_url with the request_id as a param' do subject = ServiceProviderSessionDecorator.new( sp: sp, view_context: view_context, sp_session: { request_id: 'foo' } ) - expect(subject.cancel_link_url). - to eq 'http://www.example.com/sign_up/start?request_id=foo' + expect(subject.cancel_link_path). + to eq '/sign_up/start?request_id=foo' end end end diff --git a/spec/decorators/session_decorator_spec.rb b/spec/decorators/session_decorator_spec.rb index 47f76fcf9c5..7ad3e2c2fa3 100644 --- a/spec/decorators/session_decorator_spec.rb +++ b/spec/decorators/session_decorator_spec.rb @@ -61,9 +61,9 @@ end end - describe '#cancel_link_url' do - it 'returns root url' do - expect(subject.cancel_link_url).to eq 'http://www.example.com/' + describe '#cancel_link_path' do + it 'returns root path' do + expect(subject.cancel_link_path).to eq '/' end end end diff --git a/spec/features/saml/loa1/account_creation_spec.rb b/spec/features/saml/loa1/account_creation_spec.rb index e8ad21e9077..5d6a18d4cb3 100644 --- a/spec/features/saml/loa1/account_creation_spec.rb +++ b/spec/features/saml/loa1/account_creation_spec.rb @@ -14,4 +14,19 @@ expect(current_url).to eq sign_up_start_url(request_id: sp_request_id) end end + + context 'From the enter password page', email: true do + it 'redirects to the branded start page' do + authn_request = auth_request.create(saml_settings) + visit authn_request + sp_request_id = ServiceProviderRequest.last.uuid + click_link t('sign_up.registrations.create_account') + submit_form_with_valid_email + click_confirmation_link_in_email('test@test.com') + screenshot_and_save_page + click_button t('links.cancel_account_creation') + + expect(current_url).to eq sign_up_start_url(request_id: sp_request_id) + end + end end diff --git a/spec/views/sign_up/registrations/new.html.slim_spec.rb b/spec/views/sign_up/registrations/new.html.slim_spec.rb index 27aaaf0d2bc..5d13ea5fbe1 100644 --- a/spec/views/sign_up/registrations/new.html.slim_spec.rb +++ b/spec/views/sign_up/registrations/new.html.slim_spec.rb @@ -35,9 +35,9 @@ expect(rendered).to have_xpath("//form[@autocomplete='off']") end - it 'includes a link to return to the decorated_session cancel_link_url' do + it 'includes a link to return to the decorated_session cancel_link_path' do render - expect(rendered).to have_link(t('links.cancel'), href: @decorated_session.cancel_link_url) + expect(rendered).to have_link(t('links.cancel'), href: @decorated_session.cancel_link_path) end end From 304457d7082b7fbc95b2932c56b559c5fd9cc641 Mon Sep 17 00:00:00 2001 From: Adam Biagianti Date: Mon, 5 Jun 2017 11:44:31 -0400 Subject: [PATCH 09/17] Use single link in account password reset partial **Why**: We have a new page with two choices for reactivating an account, so having two links on the /account page is redundant --- app/views/accounts/_password_reset.html.slim | 3 +-- config/locales/account/en.yml | 4 +--- config/locales/account/es.yml | 3 +-- spec/views/accounts/show.html.slim_spec.rb | 5 ++--- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/views/accounts/_password_reset.html.slim b/app/views/accounts/_password_reset.html.slim index eaa375b4a09..ab541b0f1e8 100644 --- a/app/views/accounts/_password_reset.html.slim +++ b/app/views/accounts/_password_reset.html.slim @@ -1,4 +1,3 @@ .mb4.alert.alert-warning p = t('account.index.reactivation.instructions') - p.mb0 = link_to t('account.index.reactivation.personal_key'), reactivate_account_path - p.mb0.mt2 = link_to t('account.index.reactivation.reverify'), verify_path + p.mb0 = link_to t('account.index.reactivation.link'), manage_reactivate_account_path diff --git a/config/locales/account/en.yml b/config/locales/account/en.yml index e70ce09e8d2..598b0a31909 100644 --- a/config/locales/account/en.yml +++ b/config/locales/account/en.yml @@ -15,9 +15,7 @@ en: previous_address: Previous address reactivation: instructions: Your profile was recently deactivated due to a password reset. - You can use your personal key to reactivate your profile. - personal_key: Reactivate your profile with your personal key. - reverify: No personal key? Reverify all your information instead. + link: Reactivate your profile now. ssn: Social Security Number verification: instructions: Your account requires a secret code to be verified. diff --git a/config/locales/account/es.yml b/config/locales/account/es.yml index eab2ca58c9d..27f9b56a511 100644 --- a/config/locales/account/es.yml +++ b/config/locales/account/es.yml @@ -15,8 +15,7 @@ es: previous_address: NOT TRANSLATED YET reactivation: instructions: NOT TRANSLATED YET - personal_key: NOT TRANSLATED YET - reverify: NOT TRANSLATED YET + link: NOT TRANSLATED YET ssn: NOT TRANSLATED YET verification: instructions: NOT TRANSLATED YET diff --git a/spec/views/accounts/show.html.slim_spec.rb b/spec/views/accounts/show.html.slim_spec.rb index 2ffdd21637e..4d6c9e1c826 100644 --- a/spec/views/accounts/show.html.slim_spec.rb +++ b/spec/views/accounts/show.html.slim_spec.rb @@ -92,9 +92,8 @@ it 'contains link to reactivate profile via personal key or reverification' do render - expect(rendered).to have_link(t('account.index.reactivation.personal_key'), - href: reactivate_account_path) - expect(rendered).to have_link(t('account.index.reactivation.reverify'), href: verify_path) + expect(rendered).to have_link(t('account.index.reactivation.link'), + href: manage_reactivate_account_path) end end From a2a6876858005c3fbb1b69ffa4d9b67289442656 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Fri, 2 Jun 2017 14:29:31 -0400 Subject: [PATCH 10/17] Update PSV to include issuer attributes **Why**: So we can print the name of the SP and the URL to go to --- app/controllers/concerns/idv_session.rb | 6 +++++- app/controllers/concerns/two_factor_authenticatable.rb | 6 +++++- app/services/idv/session.rb | 7 ++++--- app/services/usps_confirmation_entry.rb | 3 ++- app/services/usps_confirmation_maker.rb | 10 ++++++---- app/services/usps_exporter.rb | 4 ++++ spec/controllers/concerns/idv_step_concern_spec.rb | 4 +++- .../otp_verification_controller_spec.rb | 4 +++- .../verify/confirmations_controller_spec.rb | 6 +++++- spec/controllers/verify/finance_controller_spec.rb | 6 +++++- spec/controllers/verify/review_controller_spec.rb | 6 +++++- spec/controllers/verify/sessions_controller_spec.rb | 4 +++- spec/decorators/usps_decorator_spec.rb | 2 +- spec/features/saml/loa3_sso_spec.rb | 8 +++++++- spec/services/idv/financials_step_spec.rb | 2 +- spec/services/idv/financials_validator_spec.rb | 2 +- spec/services/idv/phone_step_spec.rb | 2 +- spec/services/idv/phone_validator_spec.rb | 2 +- spec/services/idv/profile_step_spec.rb | 2 +- spec/services/idv/session_spec.rb | 2 +- spec/services/usps_exporter_spec.rb | 5 ++++- spec/support/controller_helper.rb | 2 +- 22 files changed, 69 insertions(+), 26 deletions(-) diff --git a/app/controllers/concerns/idv_session.rb b/app/controllers/concerns/idv_session.rb index 76722ccdada..271de8555f2 100644 --- a/app/controllers/concerns/idv_session.rb +++ b/app/controllers/concerns/idv_session.rb @@ -25,7 +25,11 @@ def confirm_idv_vendor_session_started end def idv_session - @_idv_session ||= Idv::Session.new(user_session, current_user) + @_idv_session ||= Idv::Session.new( + user_session: user_session, + current_user: current_user, + issuer: sp_session[:issuer] + ) end def idv_vendor diff --git a/app/controllers/concerns/two_factor_authenticatable.rb b/app/controllers/concerns/two_factor_authenticatable.rb index 73f0761a1a2..f7a5818d6ce 100644 --- a/app/controllers/concerns/two_factor_authenticatable.rb +++ b/app/controllers/concerns/two_factor_authenticatable.rb @@ -152,7 +152,11 @@ def update_phone_attributes def update_idv_state now = Time.zone.now if idv_context? - Idv::Session.new(user_session, current_user).params['phone_confirmed_at'] = now + Idv::Session.new( + user_session: user_session, + current_user: current_user, + issuer: sp_session[:issuer] + ).params['phone_confirmed_at'] = now elsif profile_context? Idv::ProfileActivator.new(user: current_user).call end diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 7c32db0e241..0317f404eae 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -17,9 +17,10 @@ class Session vendor_session_id ].freeze - def initialize(user_session, current_user) + def initialize(user_session:, current_user:, issuer:) @user_session = user_session @current_user = current_user + @issuer = issuer @user_session[:idv] ||= new_idv_session end @@ -82,7 +83,7 @@ def create_usps_entry self.pii = Pii::Attributes.new_from_json(user_session[:decrypted_pii]) end - UspsConfirmationMaker.new(pii: pii).perform + UspsConfirmationMaker.new(pii: pii, issuer: issuer).perform end def alive? @@ -95,7 +96,7 @@ def address_mechanism_chosen? private - attr_accessor :user_session, :current_user + attr_accessor :user_session, :current_user, :issuer def new_idv_session { params: {}, step_attempts: { financials: 0, phone: 0 } } diff --git a/app/services/usps_confirmation_entry.rb b/app/services/usps_confirmation_entry.rb index c30a27fd901..2e90944da1e 100644 --- a/app/services/usps_confirmation_entry.rb +++ b/app/services/usps_confirmation_entry.rb @@ -6,7 +6,8 @@ :last_name, :otp, :state, - :zipcode + :zipcode, + :issuer ) do def self.user_access_key SessionEncryptor.user_access_key diff --git a/app/services/usps_confirmation_maker.rb b/app/services/usps_confirmation_maker.rb index 878dc3d8bcc..1fb164a1597 100644 --- a/app/services/usps_confirmation_maker.rb +++ b/app/services/usps_confirmation_maker.rb @@ -1,6 +1,7 @@ class UspsConfirmationMaker - def initialize(pii:) + def initialize(pii:, issuer:) @pii = pii + @issuer = issuer end def perform @@ -10,9 +11,9 @@ def perform private - attr_reader :pii + attr_reader :pii, :issuer - # rubocop:disable AbcSize + # rubocop:disable AbcSize, MethodLength # This method is single statement spread across many lines for readability def attributes { @@ -24,7 +25,8 @@ def attributes last_name: pii[:last_name], state: pii[:state], zipcode: pii[:zipcode], + issuer: issuer, } end - # rubocop:enable AbcSize + # rubocop:enable AbcSize, MethodLength end diff --git a/app/services/usps_exporter.rb b/app/services/usps_exporter.rb index 0c5a5c56e2b..cf15737cc88 100644 --- a/app/services/usps_exporter.rb +++ b/app/services/usps_exporter.rb @@ -42,6 +42,8 @@ def make_header_row(num_entries) def make_entry_row(entry) now = Time.zone.now due = now + OTP_MAX_VALID_DAYS.days + service_provider = ServiceProvider.from_issuer(entry.issuer) + [ CONTENT_ROW_ID, "#{entry.first_name} #{entry.last_name}", @@ -53,6 +55,8 @@ def make_entry_row(entry) entry.otp, "#{now.strftime('%-B %-e')}, #{now.year}", "#{due.strftime('%-B %-e')}, #{due.year}", + service_provider.friendly_name, + service_provider.return_to_sp_url, ] end # rubocop:enable MethodLength, AbcSize diff --git a/spec/controllers/concerns/idv_step_concern_spec.rb b/spec/controllers/concerns/idv_step_concern_spec.rb index 52c718c2cf7..5ef8adc5281 100644 --- a/spec/controllers/concerns/idv_step_concern_spec.rb +++ b/spec/controllers/concerns/idv_step_concern_spec.rb @@ -2,7 +2,9 @@ describe 'IdvStepConcern' do let(:user) { create(:user, :signed_up, email: 'old_email@example.com') } - let(:idv_session) { Idv::Session.new(subject.user_session, user) } + let(:idv_session) do + Idv::Session.new(user_session: subject.user_session, current_user: user, issuer: nil) + end module Verify class StepController < ApplicationController diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb index 10dad127122..7a1a427e8b1 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -327,7 +327,9 @@ context 'idv phone confirmation' do before do user = sign_in_as_user - idv_session = Idv::Session.new(subject.user_session, user) + idv_session = Idv::Session.new( + user_session: subject.user_session, current_user: user, issuer: nil + ) idv_session.params = { 'phone' => '+1 (555) 555-5555' } subject.user_session[:unconfirmed_phone] = '+1 (555) 555-5555' subject.user_session[:context] = 'idv' diff --git a/spec/controllers/verify/confirmations_controller_spec.rb b/spec/controllers/verify/confirmations_controller_spec.rb index be5c8ed4381..a86c74994e5 100644 --- a/spec/controllers/verify/confirmations_controller_spec.rb +++ b/spec/controllers/verify/confirmations_controller_spec.rb @@ -6,7 +6,11 @@ def stub_idv_session stub_sign_in(user) - idv_session = Idv::Session.new(subject.user_session, user) + idv_session = Idv::Session.new( + user_session: subject.user_session, + current_user: user, + issuer: nil + ) idv_session.vendor = :mock idv_session.applicant = idv_session.vendor_params idv_session.normalized_applicant_params = { first_name: 'Somebody' } diff --git a/spec/controllers/verify/finance_controller_spec.rb b/spec/controllers/verify/finance_controller_spec.rb index 4ed39b78e11..d897a3da771 100644 --- a/spec/controllers/verify/finance_controller_spec.rb +++ b/spec/controllers/verify/finance_controller_spec.rb @@ -184,7 +184,11 @@ def stub_subject user = stub_sign_in - idv_session = Idv::Session.new(subject.user_session, user) + idv_session = Idv::Session.new( + user_session: subject.user_session, + current_user: user, + issuer: nil + ) idv_session.applicant = Proofer::Applicant.new first_name: 'Some', last_name: 'One' idv_session.vendor = subject.idv_vendor.pick allow(subject).to receive(:confirm_idv_session_started).and_return(true) diff --git a/spec/controllers/verify/review_controller_spec.rb b/spec/controllers/verify/review_controller_spec.rb index 2443d8c6d01..ff31a0be5f2 100644 --- a/spec/controllers/verify/review_controller_spec.rb +++ b/spec/controllers/verify/review_controller_spec.rb @@ -30,7 +30,11 @@ } end let(:idv_session) do - idv_session = Idv::Session.new(subject.user_session, user) + idv_session = Idv::Session.new( + user_session: subject.user_session, + current_user: user, + issuer: nil + ) idv_session.profile_confirmation = true idv_session.phone_confirmation = true idv_session.financials_confirmation = true diff --git a/spec/controllers/verify/sessions_controller_spec.rb b/spec/controllers/verify/sessions_controller_spec.rb index 741bccd3af1..21ff0ee7d3b 100644 --- a/spec/controllers/verify/sessions_controller_spec.rb +++ b/spec/controllers/verify/sessions_controller_spec.rb @@ -25,7 +25,9 @@ prev_zipcode: '66666', } end - let(:idv_session) { Idv::Session.new(subject.user_session, user) } + let(:idv_session) do + Idv::Session.new(user_session: subject.user_session, current_user: user, issuer: nil) + end describe 'before_actions' do it 'includes before_actions from AccountStateChecker' do diff --git a/spec/decorators/usps_decorator_spec.rb b/spec/decorators/usps_decorator_spec.rb index ac618a9d899..890182ba499 100644 --- a/spec/decorators/usps_decorator_spec.rb +++ b/spec/decorators/usps_decorator_spec.rb @@ -8,7 +8,7 @@ profiles: [build(:profile, :active, :verified, pii: { first_name: 'Jane' })] ) - idv_session = Idv::Session.new({}, user) + idv_session = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) UspsDecorator.new(idv_session) end diff --git a/spec/features/saml/loa3_sso_spec.rb b/spec/features/saml/loa3_sso_spec.rb index 12308c64e69..85785816185 100644 --- a/spec/features/saml/loa3_sso_spec.rb +++ b/spec/features/saml/loa3_sso_spec.rb @@ -69,7 +69,9 @@ expect(current_path).to eq verify_review_path fill_in :user_password, with: user_password - click_submit_default + + expect { click_submit_default }. + to change { UspsConfirmation.count }.from(0).to(1) expect(current_url).to eq verify_confirmations_url click_acknowledge_personal_key @@ -77,6 +79,10 @@ expect(User.find_with_email(email).events.account_verified.size).to be(0) expect(current_url).to eq(account_url) expect(page).to have_content(t('account.index.verification.reactivate_button')) + + usps_confirmation_entry = UspsConfirmation.last.decrypted_entry + expect(usps_confirmation_entry.issuer). + to eq('https://rp1.serviceprovider.com/auth/saml/metadata') end it 'shows user the start page with accordion' do diff --git a/spec/services/idv/financials_step_spec.rb b/spec/services/idv/financials_step_spec.rb index 7194c88a7ec..0e1f57828a3 100644 --- a/spec/services/idv/financials_step_spec.rb +++ b/spec/services/idv/financials_step_spec.rb @@ -3,7 +3,7 @@ describe Idv::FinancialsStep do let(:user) { build(:user) } let(:idv_session) do - idvs = Idv::Session.new({}, user) + idvs = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) idvs.vendor = :mock idvs end diff --git a/spec/services/idv/financials_validator_spec.rb b/spec/services/idv/financials_validator_spec.rb index 9823110a01c..0292c6b204d 100644 --- a/spec/services/idv/financials_validator_spec.rb +++ b/spec/services/idv/financials_validator_spec.rb @@ -4,7 +4,7 @@ let(:user) { build(:user) } let(:idv_session) do - idvs = Idv::Session.new({}, user) + idvs = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) idvs.vendor = :mock idvs end diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index 6a77de8a240..ad216931aaf 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -5,7 +5,7 @@ let(:user) { build(:user) } let(:idv_session) do - idvs = Idv::Session.new({}, user) + idvs = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) idvs.vendor = :mock idvs.applicant = Proofer::Applicant.new first_name: 'Some' idvs diff --git a/spec/services/idv/phone_validator_spec.rb b/spec/services/idv/phone_validator_spec.rb index 37c6cd531ff..83b849ca116 100644 --- a/spec/services/idv/phone_validator_spec.rb +++ b/spec/services/idv/phone_validator_spec.rb @@ -4,7 +4,7 @@ let(:user) { build(:user) } let(:idv_session) do - idvs = Idv::Session.new({}, user) + idvs = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) idvs.vendor = :mock idvs end diff --git a/spec/services/idv/profile_step_spec.rb b/spec/services/idv/profile_step_spec.rb index eed055b8109..ffc71ea173d 100644 --- a/spec/services/idv/profile_step_spec.rb +++ b/spec/services/idv/profile_step_spec.rb @@ -2,7 +2,7 @@ describe Idv::ProfileStep do let(:user) { create(:user) } - let(:idv_session) { Idv::Session.new({}, user) } + let(:idv_session) { Idv::Session.new(user_session: {}, current_user: user, issuer: nil) } let(:idv_profile_form) { Idv::ProfileForm.new(idv_session.params, user) } let(:user_attrs) do { diff --git a/spec/services/idv/session_spec.rb b/spec/services/idv/session_spec.rb index b4da65e400b..7d0e6ccdea1 100644 --- a/spec/services/idv/session_spec.rb +++ b/spec/services/idv/session_spec.rb @@ -4,7 +4,7 @@ let(:user) { build(:user) } let(:user_session) { {} } - subject { described_class.new(user_session, user) } + subject { Idv::Session.new(user_session: user_session, current_user: user, issuer: nil) } describe '#method_missing' do it 'disallows un-supported attributes' do diff --git a/spec/services/usps_exporter_spec.rb b/spec/services/usps_exporter_spec.rb index 8747bbbd8db..ccb2af3630b 100644 --- a/spec/services/usps_exporter_spec.rb +++ b/spec/services/usps_exporter_spec.rb @@ -15,6 +15,7 @@ otp: 123, } end + let(:service_provider) { ServiceProvider.from_issuer('http://localhost:3000') } let(:psv_row_contents) do now = Time.zone.now due = now + UspsExporter::OTP_MAX_VALID_DAYS.days @@ -31,6 +32,8 @@ usps_entry.otp, "#{current_date}, #{now.year}", "#{due_date}, #{due.year}", + service_provider.friendly_name, + service_provider.return_to_sp_url, ] values.join('|') end @@ -50,7 +53,7 @@ describe '#run' do before do - UspsConfirmationMaker.new(pii: pii_attributes).perform + UspsConfirmationMaker.new(pii: pii_attributes, issuer: service_provider.issuer).perform end it 'creates encrypted file' do diff --git a/spec/support/controller_helper.rb b/spec/support/controller_helper.rb index a2f1ca0a4f2..668ba60645f 100644 --- a/spec/support/controller_helper.rb +++ b/spec/support/controller_helper.rb @@ -35,7 +35,7 @@ def stub_sign_in_before_2fa(user = build(:user, password: VALID_PASSWORD)) def stub_verify_steps_one_and_two(user) user_session = {} stub_sign_in(user) - idv_session = Idv::Session.new(user_session, user) + idv_session = Idv::Session.new(user_session: user_session, current_user: user, issuer: nil) idv_session.applicant = Proofer::Applicant.new first_name: 'Some', last_name: 'One' idv_session.vendor = subject.idv_vendor.pick allow(subject).to receive(:confirm_idv_session_started).and_return(true) From 87bc922303e51f865334495754e566be8d176bb4 Mon Sep 17 00:00:00 2001 From: Brian Hedberg Date: Mon, 5 Jun 2017 17:03:20 -0500 Subject: [PATCH 11/17] new footer --- .../images/sp-logos/square-gsa-dark.svg | 23 +++++++++++++++++++ app/assets/images/sp-logos/square-gsa.svg | 19 +++++++++++++++ .../stylesheets/components/_background.scss | 1 + app/assets/stylesheets/components/_color.scss | 5 ++++ app/assets/stylesheets/components/all.scss | 1 + app/views/shared/_footer_lite.html.slim | 19 ++++++++------- 6 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 app/assets/images/sp-logos/square-gsa-dark.svg create mode 100644 app/assets/images/sp-logos/square-gsa.svg create mode 100644 app/assets/stylesheets/components/_color.scss diff --git a/app/assets/images/sp-logos/square-gsa-dark.svg b/app/assets/images/sp-logos/square-gsa-dark.svg new file mode 100644 index 00000000000..9d5656f6831 --- /dev/null +++ b/app/assets/images/sp-logos/square-gsa-dark.svg @@ -0,0 +1,23 @@ + + + + GSA-logo + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/sp-logos/square-gsa.svg b/app/assets/images/sp-logos/square-gsa.svg new file mode 100644 index 00000000000..c70b85589f4 --- /dev/null +++ b/app/assets/images/sp-logos/square-gsa.svg @@ -0,0 +1,19 @@ + + + + square-gsa + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/stylesheets/components/_background.scss b/app/assets/stylesheets/components/_background.scss index 9e5a1bf3719..9c26230e10c 100644 --- a/app/assets/stylesheets/components/_background.scss +++ b/app/assets/stylesheets/components/_background.scss @@ -5,4 +5,5 @@ @media #{$breakpoint-sm} { .sm-bg-light-blue { background-color: $blue-light; } .sm-bg-none { background-color: transparent; } + .sm-bg-navy { background-color: $navy; } } diff --git a/app/assets/stylesheets/components/_color.scss b/app/assets/stylesheets/components/_color.scss new file mode 100644 index 00000000000..3e265d83e77 --- /dev/null +++ b/app/assets/stylesheets/components/_color.scss @@ -0,0 +1,5 @@ +.gray { color: $gray; } + +@media #{$breakpoint-sm} { + .sm-white { color: $white; } +} diff --git a/app/assets/stylesheets/components/all.scss b/app/assets/stylesheets/components/all.scss index 7b5dcd79dad..8937fd95861 100644 --- a/app/assets/stylesheets/components/all.scss +++ b/app/assets/stylesheets/components/all.scss @@ -4,6 +4,7 @@ @import 'border'; @import 'btn'; @import 'card'; +@import 'color'; @import 'container'; @import 'footer'; @import 'form'; diff --git a/app/views/shared/_footer_lite.html.slim b/app/views/shared/_footer_lite.html.slim index a95553a160a..b29a62b50aa 100644 --- a/app/views/shared/_footer_lite.html.slim +++ b/app/views/shared/_footer_lite.html.slim @@ -1,12 +1,15 @@ -footer.footer.bg-navy.white - .container.py2.h6 +footer.footer.bg-light-blue.sm-bg-navy + .container.h6.py1.px2.lg-px0 .clearfix - .sm-col-right.caps.pt1.pb2.sm-pt0.sm-pb0.center.caps + .col-right.caps.py0.center.caps = link_to t('links.help'), MarketingSite.help_url, - class: 'white text-decoration-none mr3', target: '_blank' + class: 'gray sm-white text-decoration-none mr3', target: '_blank' = link_to t('links.contact'), MarketingSite.contact_url, - class: 'white text-decoration-none mr3', target: '_blank' + class: 'gray sm-white text-decoration-none mr3', target: '_blank' = link_to t('links.privacy_policy'), MarketingSite.privacy_url, - class: 'white text-decoration-none', target: '_blank' - .sm-col.pt2.sm-pt0.center.border-top.sm-border-none.border-blue - = t('shared.footer_lite.gsa') + class: 'gray sm-white text-decoration-none', target: '_blank' + .col.pt0.center.flex.flex-center + = image_tag asset_url('sp-logos/square-gsa.svg'), width: 20, class: 'mr1 sm-show' + = image_tag asset_url('sp-logos/square-gsa-dark.svg'), width: 20, class: 'mr1 sm-hide' + span.sm-show.gray.sm-white + = t('shared.footer_lite.gsa') From 2a674fadaf4c73270579d6d599fc78ce45d41689 Mon Sep 17 00:00:00 2001 From: Brian Hedberg Date: Tue, 6 Jun 2017 08:34:55 -0500 Subject: [PATCH 12/17] & and translation --- app/assets/stylesheets/components/_color.scss | 2 -- app/views/shared/_footer_lite.html.slim | 18 ++++++++++++------ config/locales/links/en.yml | 2 +- config/locales/links/es.yml | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/components/_color.scss b/app/assets/stylesheets/components/_color.scss index 3e265d83e77..572de70adf1 100644 --- a/app/assets/stylesheets/components/_color.scss +++ b/app/assets/stylesheets/components/_color.scss @@ -1,5 +1,3 @@ -.gray { color: $gray; } - @media #{$breakpoint-sm} { .sm-white { color: $white; } } diff --git a/app/views/shared/_footer_lite.html.slim b/app/views/shared/_footer_lite.html.slim index b29a62b50aa..ca77f40ade4 100644 --- a/app/views/shared/_footer_lite.html.slim +++ b/app/views/shared/_footer_lite.html.slim @@ -1,15 +1,21 @@ footer.footer.bg-light-blue.sm-bg-navy .container.h6.py1.px2.lg-px0 .clearfix - .col-right.caps.py0.center.caps + .col-right.caps = link_to t('links.help'), MarketingSite.help_url, class: 'gray sm-white text-decoration-none mr3', target: '_blank' = link_to t('links.contact'), MarketingSite.contact_url, class: 'gray sm-white text-decoration-none mr3', target: '_blank' = link_to t('links.privacy_policy'), MarketingSite.privacy_url, class: 'gray sm-white text-decoration-none', target: '_blank' - .col.pt0.center.flex.flex-center - = image_tag asset_url('sp-logos/square-gsa.svg'), width: 20, class: 'mr1 sm-show' - = image_tag asset_url('sp-logos/square-gsa-dark.svg'), width: 20, class: 'mr1 sm-hide' - span.sm-show.gray.sm-white - = t('shared.footer_lite.gsa') + .col + = link_to('https://gsa.gov', + class: 'flex flex-center text-decoration-none white', + target: '_blank') do + = image_tag asset_url('sp-logos/square-gsa.svg'), + width: 20, class: 'mr1 sm-show', alt: 'GSA homepage' + = image_tag asset_url('sp-logos/square-gsa-dark.svg'), + width: 20, class: 'mr1 sm-hide', alt: 'GSA homepage' + span.sm-show + = t('shared.footer_lite.gsa') + end diff --git a/config/locales/links/en.yml b/config/locales/links/en.yml index be0a5af5ebc..9ac5c7482cf 100644 --- a/config/locales/links/en.yml +++ b/config/locales/links/en.yml @@ -17,7 +17,7 @@ en: auth_app_fallback_html: ' or %{link}.' fallback_to_sms_html: Send me a text message with the code instead fallback_to_voice_html: If you can't get text message right now, you can get a security code via %{link} - privacy_policy: Privacy and security + privacy_policy: Privacy & security remove: Remove resend: Resend email sign_in: Sign in diff --git a/config/locales/links/es.yml b/config/locales/links/es.yml index 4e6f377c099..82e7ddbfe50 100644 --- a/config/locales/links/es.yml +++ b/config/locales/links/es.yml @@ -17,7 +17,7 @@ es: auth_app_fallback_html: NOT TRANSLATED YET fallback_to_sms_html: Envíame un mensaje de texto con el código en su lugar fallback_to_voice_html: Llámame con el código en su lugar - privacy_policy: Política de privacidad + privacy_policy: Privacidad y seguridad remove: NOT TRANSLATED YET resend: Enviar de nuevo sign_in: Iniciar sesión From 8c3501b5d0a478cda423673cc8effc136f3064e0 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Wed, 7 Jun 2017 17:00:32 -0400 Subject: [PATCH 13/17] Add scripts to help translate markdown files **Why**: Useful for the static site --- Gemfile | 1 + Gemfile.lock | 2 + lib/i18n_converter.rb | 34 ++++++++++++++-- scripts/i18n-md-to-xml | 5 +++ scripts/i18n-xml-to-md | 5 +++ spec/lib/i18n_converter_spec.rb | 70 +++++++++++++++++++++++++++++++++ 6 files changed, 114 insertions(+), 3 deletions(-) create mode 100755 scripts/i18n-md-to-xml create mode 100755 scripts/i18n-xml-to-md diff --git a/Gemfile b/Gemfile index 9517570607c..4348815513d 100644 --- a/Gemfile +++ b/Gemfile @@ -80,6 +80,7 @@ end group :development, :test do gem 'bullet' + gem 'front_matter_parser' gem 'i18n-tasks' gem 'mailcatcher', require: false gem 'pry-byebug' diff --git a/Gemfile.lock b/Gemfile.lock index 5ae0b8295c1..672caac3417 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -265,6 +265,7 @@ GEM thor (~> 0.14) formatador (0.2.5) foundation_emails (2.2.1.0) + front_matter_parser (0.1.0) geocoder (1.4.4) get_process_mem (0.2.1) gibberish (2.1.0) @@ -678,6 +679,7 @@ DEPENDENCIES fasterer figaro foundation_emails + front_matter_parser gibberish guard-rspec gyoku diff --git a/lib/i18n_converter.rb b/lib/i18n_converter.rb index 9c5514598fd..6342436134e 100644 --- a/lib/i18n_converter.rb +++ b/lib/i18n_converter.rb @@ -1,5 +1,6 @@ require 'yaml' require 'active_support/core_ext/hash/conversions' +require 'front_matter_parser' class I18nConverter def initialize(stdin:, stdout:) @@ -10,9 +11,8 @@ def initialize(stdin:, stdout:) def xml_to_yml return if bad_usage?(in_format: :xml, out_format: :yml) - data = Hash.from_xml(stdin.read) - data_hash = data['hash'] - data = data_hash if data_hash + data = read_xml + stdout.puts YAML.dump(data) end @@ -23,6 +23,27 @@ def yml_to_xml stdout.puts data.to_xml end + def md_to_xml + return if bad_usage?(in_format: :md, out_format: :xml) + + parsed = FrontMatterParser::Parser.new(:md).call(stdin.read) + data = { + front_matter: parsed.front_matter, + content: parsed.content, + } + stdout.puts data.to_xml + end + + def xml_to_md + return if bad_usage?(in_format: :xml, out_format: :md) + + data = read_xml + + stdout.puts YAML.dump(data['front_matter']) + stdout.puts '---' + stdout.puts data['content'] + end + private attr_reader :stdin, :stdout @@ -36,4 +57,11 @@ def bad_usage?(in_format:, out_format:) # rubocop:enable Rails/Exit true end + + def read_xml + data = Hash.from_xml(stdin.read) + data_hash = data['hash'] + data = data_hash if data_hash + data + end end diff --git a/scripts/i18n-md-to-xml b/scripts/i18n-md-to-xml new file mode 100755 index 00000000000..8d8a8bf24e8 --- /dev/null +++ b/scripts/i18n-md-to-xml @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.expand_path('../lib', File.dirname(__FILE__))) +require 'i18n_converter' + +I18nConverter.new(stdin: STDIN, stdout: STDOUT).md_to_xml diff --git a/scripts/i18n-xml-to-md b/scripts/i18n-xml-to-md new file mode 100755 index 00000000000..c9e9d3e629b --- /dev/null +++ b/scripts/i18n-xml-to-md @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.expand_path('../lib', File.dirname(__FILE__))) +require 'i18n_converter' + +I18nConverter.new(stdin: STDIN, stdout: STDOUT).xml_to_md diff --git a/spec/lib/i18n_converter_spec.rb b/spec/lib/i18n_converter_spec.rb index 5448449019b..428dbfa9d0b 100644 --- a/spec/lib/i18n_converter_spec.rb +++ b/spec/lib/i18n_converter_spec.rb @@ -31,6 +31,30 @@ XML end + let(:markdown_md) do + <<~MD + --- + title: Title + description: Description + --- + Some **fancy** markdown + MD + end + + let(:markdown_xml) do + <<~XML + + + + Title + Description + + Some **fancy** markdown + + + XML + end + subject(:converter) { I18nConverter.new(stdin: stdin, stdout: stdout) } describe '.yml_to_xml' do @@ -78,4 +102,50 @@ expect(stdout.string).to eq(translation_yml) end end + + describe '.md_to_xml' do + let(:stdout) { StringIO.new } + let(:stdin) { StringIO.new(markdown_md) } + + context 'with a TTY on STDIN' do + let(:stdin) { instance_double('IO', tty?: true) } + + it 'prints an error and exits' do + expect(converter).to receive(:exit).with(1) + + converter.md_to_xml + + expect(stdout.string.chomp).to eq("Usage: cat en.md | #{$PROGRAM_NAME} > output.xml") + end + end + + it 'outputs XML' do + converter.md_to_xml + + expect(stdout.string).to eq(markdown_xml) + end + end + + describe '.xml_to_md' do + let(:stdout) { StringIO.new } + let(:stdin) { StringIO.new(markdown_xml) } + + context 'with a TTY on STDIN' do + let(:stdin) { instance_double('IO', tty?: true) } + + it 'prints an error and exits' do + expect(converter).to receive(:exit).with(1) + + converter.xml_to_md + + expect(stdout.string.chomp).to eq("Usage: cat en.xml | #{$PROGRAM_NAME} > output.md") + end + end + + it 'outputs XML' do + converter.xml_to_md + + expect(stdout.string).to eq(markdown_md) + end + end end From 2fad4f1047603533a967694e126a578d4c665281 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Thu, 8 Jun 2017 10:07:34 -0400 Subject: [PATCH 14/17] Register OIDC example app in deployed environments **Why**: Will simplify testing --- config/service_providers.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/config/service_providers.yml b/config/service_providers.yml index bdc1760c0af..15d12edf6b8 100644 --- a/config/service_providers.yml +++ b/config/service_providers.yml @@ -308,6 +308,20 @@ production: attribute_bundle: - email + 'urn:gov:gsa:openidconnect:sp:sinatra': + agency: 'GSA' + cert: 'sp_sinatra_demo' + friendly_name: 'Example Sinatra OIDC App' + logo: '18f.svg' + redirect_uris: + - 'http://localhost:9292/' + - 'https://sp-oidc-sinatra.dev.login.gov/' + - 'https://sp-oidc-sinatra.dm.login.gov/' + - 'https://sp-oidc-sinatra.int.login.gov/' + - 'https://sp-oidc-sinatra.pt.login.gov/' + - 'https://sp-oidc-sinatra.qa.login.gov/' + - 'https://sp-oidc-sinatra.staging.login.gov/' + # CBP Jobs 'urn:gov:dhs.cbp.jobs:openidconnect:cert': redirect_uris: From 75ea3c6bf699aba4a7cd4d8617fd6d910821b0ce Mon Sep 17 00:00:00 2001 From: Tom Black Date: Tue, 6 Jun 2017 16:11:40 -0400 Subject: [PATCH 15/17] Add CBP GOES pre-prod SP --- certs/sp/cbp_goes_pre_prod.crt | 20 ++++++++++++++++++++ config/service_providers.yml | 9 +++++++++ 2 files changed, 29 insertions(+) create mode 100644 certs/sp/cbp_goes_pre_prod.crt diff --git a/certs/sp/cbp_goes_pre_prod.crt b/certs/sp/cbp_goes_pre_prod.crt new file mode 100644 index 00000000000..2b5cf18ade8 --- /dev/null +++ b/certs/sp/cbp_goes_pre_prod.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQDCCAigCCQDO/Keu09NJ2TANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCVkExFDASBgNVBAcMC1NwcmluZ2ZpZWxkMQwwCgYDVQQKDAND +QlAxDDAKBgNVBAsMA09JVDEUMBIGA1UEAwwLYXdzLWplbmtpbnMwHhcNMTcwNDEw +MTU1OTQ3WhcNMTgwNDEwMTU1OTQ3WjBiMQswCQYDVQQGEwJVUzELMAkGA1UECAwC +VkExFDASBgNVBAcMC1NwcmluZ2ZpZWxkMQwwCgYDVQQKDANDQlAxDDAKBgNVBAsM +A09JVDEUMBIGA1UEAwwLYXdzLWplbmtpbnMwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDCdPMeAR6GqvSCyoEWeB1EhcejwxrcMrRtXXfsIn53byCDNsTi +0tr/2cl5EO2+s5BKJv8f1q4BmR4ThCupdo/7pc8YB9mZiDKYTeornzSS632F0EZA +ZCK+l/g1RKSejkJVqz8SFordkT1tnICZa0tHEVGBfO7Gd7g5vM2vk3ss2AEUCS9y +/eh3A/3p7Cr3S73dcS+ta1ojA7wv1fkKx5gvfEMZ9AjCYBs3klOfhsG2zEsorbCT +N+Jj3EJEErrsEBQ+GuMoZ18tMh5LeLMHVBRzaa60p/Jpq3or50rDE4pxVcUm3O4m +wbgz+1spU/Db62rbVPJlL4ki/4o/H+kLs6rrAgMBAAEwDQYJKoZIhvcNAQEFBQAD +ggEBAKo1OPFIlU4TMwsDJ2TgAqz3xW3+ATyUZ51KsPm2BKVLMReM6Eg87M6Ebd9j +Sg1zomv5IHfjBtvUo8Eb3lxArgKkInsrJ4GJgAZLO3a3PjKgLxd0lf7WZmwH/u5N +QkMgqogNkIJY3a9ORhPEECxzuHKqvbYJ6aKQfJDgkpnJeuswaad9VAxFq/P775Ey +0ydQGQwi8s2qc1WUl3R/IcbYpRX2aoirO+LSdkF8b1zeVm5haZgyW9g/yJaEx7c/ +lTcuQoT5JVXM35M0WVeVUxTouUNfRlhdlOsNkBybW8biJr6QWfoBEXtAQI9G1iw/ +5HvDnrxOzshTJ0McgfWAfgGInt0= +-----END CERTIFICATE----- diff --git a/config/service_providers.yml b/config/service_providers.yml index bdc1760c0af..ddd9ac50f32 100644 --- a/config/service_providers.yml +++ b/config/service_providers.yml @@ -363,3 +363,12 @@ production: - dob - ssn - phone + + # CBP GOES + 'urn:gov:dhs.cbp.jobs:openidconnect:jenkins-pspd-credential-service': + friendly_name: 'CBP PSPD Trusted Traveler Programs' + agency: 'DHS' + logo: 'cbp.png' + cert: 'cbp_goes_pre_prod' + redirect_uris: + - 'http://10.156.152.27/login' From 2aa930c9299ef307fdd7e552c4c737ffca869ede Mon Sep 17 00:00:00 2001 From: amos Date: Thu, 18 May 2017 12:53:38 -0700 Subject: [PATCH 16/17] Updates User Flows tools Adds web export task (compatible with Federalist) Expands specs to cover new mail activation flow Relaxes style constraints for User Flows tool --- .codeclimate.yml | 12 +- .rubocop.yml | 2 + README.md | 10 + config/environments/test.rb | 6 +- lib/rspec/formatters/user_flow_formatter.rb | 4 +- lib/tasks/user_flows.rake | 8 + lib/user_flow_exporter.rb | 91 +++++ .../flows/sp_authentication_flows_spec.rb | 362 ++++++++++++++---- spec/features/flows/visitor_flows_spec.rb | 55 +++ spec/spec_helper.rb | 1 + 10 files changed, 480 insertions(+), 71 deletions(-) create mode 100644 lib/user_flow_exporter.rb diff --git a/.codeclimate.yml b/.codeclimate.yml index 92e3e98c6a4..a4b07479f97 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,6 +1,11 @@ engines: brakeman: enabled: true + exclude_paths: + # Excluding User Flows tools since these are not loaded + # except when explicitly called from the User Flow rake tasks + - 'lib/user_flow_exporter.rb' + - 'lib/rspec/formatters/user_flow_formatter.rb' bundler-audit: enabled: true coffeelint: @@ -19,6 +24,8 @@ engines: - 'node_modules/**/*' - 'db/schema.rb' - 'app/forms/password_form.rb' + - 'lib/user_flow_exporter.rb' + - 'lib/rspec/formatters/user_flow_formatter.rb' eslint: enabled: true fixme: @@ -38,6 +45,8 @@ engines: exclude_paths: - 'spec/**/*' - 'db/migrate/*' + - 'lib/user_flow_exporter.rb' + - 'lib/rspec/formatters/user_flow_formatter.rb' rubocop: enabled: true scss-lint: @@ -50,4 +59,5 @@ ratings: - '**.rb' - '**.go' exclude_paths: - - 'lib/rspec/formatters/*' + - 'lib/user_flow_exporter.rb' + - 'lib/rspec/formatters/user_flow_formatter.rb' diff --git a/.rubocop.yml b/.rubocop.yml index f52cbfadd9d..cd7b91340b4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,6 +14,8 @@ AllCops: - 'config/initializers/devise.rb' - 'db/migrate/*' - 'spec/services/pii/nist_encryption_spec.rb' + - 'lib/rspec/user_flow_formatter.rb' + - 'lib/user_flow_exporter.rb' TargetRubyVersion: 2.3 UseCache: true diff --git a/README.md b/README.md index f595a88634a..c4aa2aba38e 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,16 @@ $ RAILS_ASSET_HOST=localhost:3000 rake spec:user_flows Then, visit http://localhost:3000/user_flows in your browser! +##### Exporting + +The user flows tool also has an export feature which allows you to export everything for the web. You may host these assets with someting like [`simplehttpserver`](https://www.npmjs.com/package/simplehttpserver) or publish to [Federalist](https://federalist.18f.gov/). To publish user flows for Federalist, first make sure the application is running locally (eg. localhost:3000) and run: + +``` +$ RAILS_ASSET_HOST=localhost:3000 FEDERALIST_PATH=/site/user/repository rake spec:user_flows:web +``` + +This will output your site to `public/site/user/repository` for quick publishing to [Federalist](https://federalist-docs.18f.gov/pages/using-federalist/). To test compatibility, run `simplehttpserver` from the app's `public` folder and visit `http://localhost:8000//user_flows` in your browser. + ### Load testing We provide some [Locust.io] Python scripts you can run to test how the diff --git a/config/environments/test.rb b/config/environments/test.rb index 522784f0c36..e23d43c3c47 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -17,14 +17,16 @@ config.action_mailer.asset_host = Figaro.env.mailer_domain_name config.action_mailer.default_options = { from: Figaro.env.email_from } + config.assets.debug = true + if ENV.key?('RAILS_ASSET_HOST') config.action_controller.asset_host = ENV['RAILS_ASSET_HOST'] else config.action_controller.asset_host = '//' end - config.assets.debug = true - config.assets.digest = true + config.assets.digest = ENV.key?('RAILS_DISABLE_ASSET_DIGEST') ? false : true + config.middleware.use RackSessionAccess::Middleware config.lograge.enabled = true config.lograge.custom_options = ->(event) { event.payload } diff --git a/lib/rspec/formatters/user_flow_formatter.rb b/lib/rspec/formatters/user_flow_formatter.rb index aea9bd71889..3176080cdfc 100644 --- a/lib/rspec/formatters/user_flow_formatter.rb +++ b/lib/rspec/formatters/user_flow_formatter.rb @@ -23,10 +23,10 @@ class UserFlowFormatter < RSpec::Core::Formatters::DocumentationFormatter :stop def initialize(output) - @html = '' \ + @html = '' \ '' \ '

' \ - '' \ + '' \ '/ user flows

' \ '
'
     @user_flows_html_file = Capybara.save_path.join('index.html').to_s
diff --git a/lib/tasks/user_flows.rake b/lib/tasks/user_flows.rake
index d71a0119412..fc5a38477ca 100644
--- a/lib/tasks/user_flows.rake
+++ b/lib/tasks/user_flows.rake
@@ -7,5 +7,13 @@ unless Rails.env.production?
                         --require ./lib/rspec/formatters/user_flow_formatter.rb
                         --format UserFlowFormatter]
     end
+
+    desc 'Exports user flows for the web'
+    task 'user_flows:web' do |t|
+      ENV['RAILS_DISABLE_ASSET_DIGEST'] = 'true'
+      require './lib/user_flow_exporter'
+      Rake::Task['spec:user_flows'].invoke
+      UserFlowExporter.run
+    end
   end
 end
diff --git a/lib/user_flow_exporter.rb b/lib/user_flow_exporter.rb
new file mode 100644
index 00000000000..b1b089b1e65
--- /dev/null
+++ b/lib/user_flow_exporter.rb
@@ -0,0 +1,91 @@
+# This module is part of the User Flows toolchest
+# 
+# UserFlowExporter.run - scrapes user flows for use on the web
+# 
+# Dependencies:
+#   - Must be running the application locally eg (localhost:3000)
+#   - Must have wget installed and available on your PATH
+# 
+# Executing:
+#   Start the application with:
+#     $ make run
+#   Export flows with:
+#     $ RAILS_ASSET_HOST=localhost:3000 FEDERALIST_PATH=/site/user/repo rake spec:user_flows:web
+#   Use the files output to public/ in a Github repo connected to Federalist
+#     $ cp -r ./public/site/user/repo ~/code/login-user-flows
+#   And commit the changes in the Federalist repo!
+
+module UserFlowExporter
+  ASSET_HOST = ENV['RAILS_ASSET_HOST'] || 'localhost:3000'
+  # Coming soon: signal testing for different devices
+  # USER_AGENT = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092416 Firefox/3.0.3"
+  FEDERALIST_PATH = ENV['FEDERALIST_PATH'] || '/flows_export/'
+
+  def self.run
+    Kernel.puts "Preparing to scrape user flows...\n"
+    url = "http://#{ASSET_HOST}/user_flows/"
+    # The web-friendly flows are still output to the public directory
+    # in order to quickly test the content by visiting your locally
+    # hosted application (eg. localhost:3000/site/18f/identity-ux/user_flows)
+
+    if FEDERALIST_PATH[0] != '/'
+      raise 'Federalist path must start with a slash (eg. /site/18f/identity-ux)'
+    end
+
+    output_dir = "public#{FEDERALIST_PATH}"
+
+    # -r = recursively mirrors site
+    # -H = span hosts (e.g. include assets from other domains) 
+    # -p = download all assets associated with the page
+    # --no-host-directories = removes domain prefix from output path
+    # -P = output prefix (a.k.a the directory to dump the assets)
+    # --domains = whitelist of domains to include when following links
+    scrape_cmd = "wget -r -H -p --no-host-directories " \
+                "-P '#{output_dir}' " \
+                "--domains 'localhost' " \
+                "'#{url}'"
+    system(scrape_cmd)
+
+    massage_html(output_dir)
+    massage_assets(output_dir)
+
+    Kernel.puts 'Done! The user flows are now prepared for use on the interwebs!'
+  end
+
+  private
+
+  def self.massage_html(dir)
+    Dir.glob("#{dir}/**/*.html") do |html|
+      File.open(html) do |f|
+        contents = File.read(f.path)
+        contents.gsub!("http://#{ASSET_HOST}/", "#{FEDERALIST_PATH}/")
+        contents.gsub!('.css?body=1', '.css')
+        contents.gsub!('.js?body=1', '.js')
+        contents.gsub!('href="/assets/', "href=\"#{FEDERALIST_PATH}/assets/")
+        contents.gsub!('src="/assets/', "src=\"#{FEDERALIST_PATH}/assets/")
+        contents.gsub!("href='/user_flows/", "href='#{FEDERALIST_PATH}/user_flows/")
+
+        contents.gsub!("", "")
+
+        File.open(f.path, "w") {|file| file.puts contents }
+        Kernel.puts "Updated #{f.path} references"
+      end
+    end
+  end
+
+  def self.massage_assets(dir)
+    Dir.glob("#{dir}/assets/**/**") do |file|
+      if file[-11..-1] == '.css?body=1'
+        new_filename = file.gsub('.css?body=1', '.css')
+        `mv #{file} #{new_filename}`
+        Kernel.puts "Moved #{file} to #{new_filename}"
+      end
+
+      if file[-10..-1] == '.js?body=1'
+        new_filename = file.gsub('.js?body=1', '.js')
+        `mv #{file} #{new_filename}`
+        Kernel.puts "Moved #{file} to #{new_filename}"
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/features/flows/sp_authentication_flows_spec.rb b/spec/features/flows/sp_authentication_flows_spec.rb
index c8146a4cc59..49fdc1dd6f8 100644
--- a/spec/features/flows/sp_authentication_flows_spec.rb
+++ b/spec/features/flows/sp_authentication_flows_spec.rb
@@ -1,145 +1,375 @@
 require 'rails_helper'
-
+include IdvHelper
 include SamlAuthHelper
 
-feature 'SP-initiated authentication with login.gov', devise: true, user_flow: true do
+feature 'SP-initiated authentication with login.gov', user_flow: true do
   context 'with a valid SP' do
-    before do
-      visit authnrequest_get
-    end
-
-    it 'prompts the user to create an account or sign in' do
-      screenshot_and_save_page
-    end
-
-    context 'when choosing Create Account' do
+    context 'when LOA3' do
       before do
-        click_link t('sign_up.registrations.create_account')
+        visit loa3_authnrequest
       end
 
-      it 'displays an interstitial page with information' do
+      it 'prompts the user to create an account or sign in' do
         screenshot_and_save_page
       end
 
-      it 'prompts for email address' do
-        screenshot_and_save_page
-      end
-
-      context 'with a valid email address submitted' do
+      context 'when choosing Create Account' do
         before do
-          @email = Faker::Internet.safe_email
-          fill_in 'Email', with: @email
-          click_button t('forms.buttons.submit.default')
-          @user = User.find_with_email(@email)
+          click_link t('sign_up.registrations.create_account')
         end
 
-        it 'informs the user to check email' do
+        it 'prompts for email address' do
           screenshot_and_save_page
         end
 
-        context 'with a confirmed email address' do
+        context 'with a valid email address submitted' do
           before do
-            confirm_last_user
+            @email = Faker::Internet.safe_email
+            fill_in 'Email', with: @email
+            click_button t('forms.buttons.submit.default')
+            @user = User.find_with_email(@email)
           end
 
-          it 'prompts the user for a password' do
+          it 'informs the user to check email' do
             screenshot_and_save_page
           end
 
-          context 'with a valid password' do
+          context 'with a confirmed email address' do
             before do
-              fill_in 'password_form_password', with: Features::SessionHelper::VALID_PASSWORD
-              click_button t('forms.buttons.continue')
+              confirm_last_user
             end
 
-            it 'prompts the user to configure 2FA' do
+            it 'prompts the user for a password' do
               screenshot_and_save_page
             end
 
-            context 'with a valid phone number' do
+            context 'with a valid password' do
               before do
-                fill_in 'Phone', with: Faker::PhoneNumber.cell_phone
+                fill_in 'password_form_password', with: Features::SessionHelper::VALID_PASSWORD
+                click_button t('forms.buttons.continue')
               end
 
-              context 'with SMS delivery' do
-                before do
-                  choose t('devise.two_factor_authentication.otp_delivery_preference.sms')
-                  click_send_security_code
-                end
-
-                it 'prompts for OTP' do
-                  screenshot_and_save_page
-                end
+              it 'prompts the user to configure 2FA' do
+                screenshot_and_save_page
               end
 
-              context 'with Voice delivery' do
+              context 'with a valid phone number' do
                 before do
-                  choose t('devise.two_factor_authentication.otp_delivery_preference.voice')
-                  click_send_security_code
+                  fill_in 'Phone', with: Faker::PhoneNumber.cell_phone
                 end
 
-                it 'prompts for OTP' do
-                  screenshot_and_save_page
+                context 'with SMS delivery' do
+                  before do
+                    choose t('devise.two_factor_authentication.otp_delivery_preference.sms')
+                    click_send_security_code
+                  end
+
+                  it 'prompts for OTP' do
+                    screenshot_and_save_page
+                  end
+
+                  context 'with valid OTP confirmation' do
+                    before do
+                      fill_in 'code', with: @user.reload.direct_otp
+                      click_button t('forms.buttons.submit.default')
+                    end
+
+                    it 'prompts the user to verify oneself' do
+                      screenshot_and_save_page
+                    end
+
+                    context 'when choosing Yes, continue' do
+                      before do
+                        click_link t('idv.index.continue_link')
+                      end
+
+                      it 'prompts for personal information' do
+                        screenshot_and_save_page
+                      end
+
+                      context 'with valid personal information entered' do
+                        before do
+                          fill_in t('idv.form.first_name'), with: Faker::Name.first_name
+                          fill_in t('idv.form.last_name'), with: Faker::Name.last_name
+                          fill_in 'profile_address1', with: '123 Main St'
+                          fill_in 'profile_city', with: Faker::Address.city
+                          find('#profile_state').find(:xpath,
+                                                      "option[#{(1..50).to_a.sample}]").
+                            select_option
+                          fill_in 'profile_zipcode', with: Faker::Address.zip_code
+                          fill_in t('idv.form.dob'), with: "09/09/#{(1900..2000).to_a.sample}"
+                          fill_in 'profile_ssn', with: "999-99-#{(1000..9999).to_a.sample}"
+                          click_button t('forms.buttons.continue')
+                        end
+
+                        it 'prompts for the last 8 digits of a credit card' do
+                          screenshot_and_save_page
+                        end
+
+                        context 'with last 8 digits of credit card' do
+                          before do
+                            fill_out_financial_form_ok
+                          end
+
+                          it 'prompts to activate account by phone or mail' do
+                            screenshot_and_save_page
+                          end
+                        end
+
+                        context 'without a credit card' do
+                          before do
+                            click_link t('idv.form.use_financial_account')
+                          end
+
+                          it 'prompts user to provide a financial account number' do
+                            screenshot_and_save_page
+                          end
+
+                          context 'with a valid financial account' do
+                            before do
+                              select t('idv.form.mortgage'), from: 'idv_finance_form_finance_type'
+                              fill_in 'idv_finance_form_mortgage', with: '12345678'
+                              # click_idv_continue doesn't work with the JavaScript on this page
+                              # and enabling js: true causes unexpected behavior
+                              form = page.find('#new_idv_finance_form')
+                              class << form
+                                def submit!
+                                  Capybara::RackTest::Form.new(driver, native).submit({})
+                                end
+                              end
+                              form.submit!
+                            end
+
+                            it 'prompts to activate account by phone or mail' do
+                              screenshot_and_save_page
+                            end
+
+                            context 'when activating by phone' do
+                              before do
+                                click_idv_address_choose_phone
+                              end
+
+                              it 'prompts the user to confirm or enter phone number' do
+                                screenshot_and_save_page
+                              end
+                            end
+
+                            context 'when activating by mail' do
+                              before do
+                                click_idv_address_choose_usps
+                              end
+
+                              it 'prompts the user to confirm' do
+                                screenshot_and_save_page
+                              end
+
+                              context 'when confirming to mail' do
+                                before do
+                                  click_on t('idv.buttons.mail.send')
+                                end
+
+                                it 'prompts user for password to encrypt profile' do
+                                  screenshot_and_save_page
+                                end
+
+                                context 'when confirming password' do
+                                  before do
+                                    fill_in 'user_password',
+                                            with: Features::SessionHelper::VALID_PASSWORD
+                                    click_button t('forms.buttons.submit.default')
+                                  end
+
+                                  it 'provides a new personal key and prompts for verification' do
+                                    screenshot_and_save_page
+                                  end
+
+                                  context 'when clicking Continue' do
+                                    before do
+                                      click_acknowledge_personal_key
+                                    end
+
+                                    it 'displays the user profile' do
+                                      screenshot_and_save_page
+                                    end
+                                  end
+                                end
+                              end
+                            end
+
+                            # Disabling this spec because of js: true issue
+                            # Will re-enable this once resolved
+                            # context 'when choosing to cancel' do
+                            #   before do
+                            #     click_button t('links.cancel_idv')
+                            #   end
+
+                            #   it 'prompts to continue verification or visit profile' do
+                            #     screenshot_and_save_page
+                            #   end
+                            # end
+                          end
+                        end
+                      end
+
+                      context 'with invalid personal information entered' do
+                        before do
+                          fill_out_idv_form_fail
+                          click_button t('forms.buttons.continue')
+                        end
+
+                        it 'presents a modal with current retries remaining' do
+                          screenshot_and_save_page
+                        end
+                      end
+                    end
+                  end
                 end
               end
             end
           end
         end
       end
+
+      # context 'when choosing to sign in' do
+      #   TODO: duplicate scenarios from Create Account here
+      # end
     end
 
-    context 'when choosing to sign in' do
+    context 'when LOA1' do
       before do
-        @user = create(:user, :signed_up)
-        click_link t('links.sign_in')
+        visit authnrequest_get
+      end
+
+      it 'prompts the user to create an account or sign in' do
+        screenshot_and_save_page
       end
 
-      context 'with valid credentials entered' do
+      context 'when choosing Create Account' do
         before do
-          fill_in_credentials_and_submit(@user.email, @user.password)
+          click_link t('sign_up.registrations.create_account')
         end
 
-        it 'prompts for 2FA delivery method' do
+        it 'prompts for email address' do
           screenshot_and_save_page
         end
 
-        context 'with SMS OTP selected (default)' do
-          it 'prompts for OTP verification' do
+        context 'with a valid email address submitted' do
+          before do
+            @email = Faker::Internet.safe_email
+            fill_in 'Email', with: @email
+            click_button t('forms.buttons.submit.default')
+            @user = User.find_with_email(@email)
+          end
+
+          it 'informs the user to check email' do
             screenshot_and_save_page
           end
 
-          context 'with valid OTP confirmation' do
+          context 'with a confirmed email address' do
             before do
-              fill_in 'code', with: @user.reload.direct_otp
-              click_button t('forms.buttons.submit.default')
+              confirm_last_user
             end
 
-            xit 'redirects back to SP' do
+            it 'prompts the user for a password' do
               screenshot_and_save_page
             end
+
+            context 'with a valid password' do
+              before do
+                fill_in 'password_form_password', with: Features::SessionHelper::VALID_PASSWORD
+                click_button t('forms.buttons.continue')
+              end
+
+              it 'prompts the user to configure 2FA' do
+                screenshot_and_save_page
+              end
+
+              context 'with a valid phone number' do
+                before do
+                  fill_in 'Phone', with: Faker::PhoneNumber.cell_phone
+                end
+
+                context 'with SMS delivery' do
+                  before do
+                    choose t('devise.two_factor_authentication.otp_delivery_preference.sms')
+                    click_send_security_code
+                  end
+
+                  it 'prompts for OTP' do
+                    screenshot_and_save_page
+                  end
+                end
+
+                context 'with Voice delivery' do
+                  before do
+                    choose t('devise.two_factor_authentication.otp_delivery_preference.voice')
+                    click_send_security_code
+                  end
+
+                  it 'prompts for OTP' do
+                    screenshot_and_save_page
+                  end
+                end
+              end
+            end
           end
         end
       end
 
-      context 'without a valid username and password' do
-        context 'when choosing "Forgot your password?"' do
+      context 'when choosing to sign in' do
+        before do
+          @user = create(:user, :signed_up)
+          click_link t('links.sign_in')
+        end
+
+        context 'with valid credentials entered' do
           before do
-            click_link t('links.passwords.forgot')
+            fill_in_credentials_and_submit(@user.email, @user.password)
           end
 
-          it 'prompts for my email address' do
+          it 'prompts for 2FA delivery method' do
             screenshot_and_save_page
           end
 
-          context 'with not_a_real_email_dot.com submitted' do
+          context 'with SMS OTP selected (default)' do
+            it 'prompts for OTP verification' do
+              screenshot_and_save_page
+            end
+
+            context 'with valid OTP confirmation' do
+              before do
+                fill_in 'code', with: @user.reload.direct_otp
+                click_button t('forms.buttons.submit.default')
+              end
+
+              # Skipping since we have nothing to show: this occurs on the SP
+              xit 'redirects back to SP' do
+                screenshot_and_save_page
+              end
+            end
+          end
+        end
+
+        context 'without a valid username and password' do
+          context 'when choosing "Forgot your password?"' do
             before do
-              fill_in 'password_reset_email_form_email', with: 'not_a_real_email_dot.com'
-              click_button t('forms.buttons.continue')
+              click_link t('links.passwords.forgot')
             end
 
-            it 'displays a useful error' do
+            it 'prompts for my email address' do
               screenshot_and_save_page
             end
+
+            context 'with not_a_real_email_dot.com submitted' do
+              before do
+                fill_in 'password_reset_email_form_email', with: 'not_a_real_email_dot.com'
+                click_button t('forms.buttons.continue')
+              end
+
+              it 'displays a useful error' do
+                screenshot_and_save_page
+              end
+            end
           end
         end
       end
diff --git a/spec/features/flows/visitor_flows_spec.rb b/spec/features/flows/visitor_flows_spec.rb
index 913e836d63b..089cd9c0f61 100644
--- a/spec/features/flows/visitor_flows_spec.rb
+++ b/spec/features/flows/visitor_flows_spec.rb
@@ -129,4 +129,59 @@
       end
     end
   end
+
+  context 'when choosing \'Forgot your password?' do
+    before do
+      visit new_user_password_path
+    end
+
+    it 'prompts for email address' do
+      screenshot_and_save_page
+    end
+
+    context 'when submitting email for an existing account' do
+      before do
+        @user = create(:user, :signed_up)
+        fill_in 'Email', with: @user.email
+        click_button t('forms.buttons.continue')
+      end
+
+      it 'informs the user to check their email' do
+        screenshot_and_save_page
+      end
+
+      context 'when following link in email', email: true do
+        before do
+          open_last_email
+          click_email_link_matching(/reset_password_token/)
+        end
+
+        it 'prompts the user to enter a new password' do
+          screenshot_and_save_page
+        end
+
+        context 'when submitting a valid password' do
+          before do
+            fill_in t('forms.passwords.edit.labels.password'), with: 'NewVal!dPassw0rd'
+            click_button t('forms.passwords.edit.buttons.submit')
+          end
+
+          it 'redirects to the homepage with a helpful message' do
+            screenshot_and_save_page
+          end
+        end
+      end
+    end
+
+    context 'when submitting email not associated with an account' do
+      before do
+        fill_in 'Email', with: 'non-existent-email@example.com'
+        click_button t('forms.buttons.continue')
+      end
+
+      it 'informs the user to check their email' do
+        screenshot_and_save_page
+      end
+    end
+  end
 end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 72a6ca81653..50dce1ca0bd 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -3,6 +3,7 @@
   SimpleCov.start 'rails' do
     add_filter '/config/'
     add_filter '/lib/rspec/formatters/user_flow_formatter.rb'
+    add_filter '/lib/user_flow_exporter.rb'
   end
 end
 

From c3df25b58a1bf97b54c2ac6b81fc54589c8f1538 Mon Sep 17 00:00:00 2001
From: Zach Margolis 
Date: Tue, 6 Jun 2017 14:09:52 -0400
Subject: [PATCH 17/17] Make sure gpg is installed on macOS

**Why**: We use gpg in the app now
---
 Dockerfile | 3 +++
 bin/setup  | 7 +++++--
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 52a26d408ef..75f3b3a2a9b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -33,5 +33,8 @@ RUN bundle install --jobs=20 --retry=5 --frozen --without deploy production
 
 COPY . /upaya
 
+RUN gpg --dearmor < keys/equifax_gpg.pub.example > keys/equifax_gpg.pub.bin
+RUN gpg --batch --import keys/equifax_gpg.example
+
 EXPOSE 3000
 CMD ["rackup", "config.ru", "--host", "0.0.0.0", "--port", "3000"]
diff --git a/bin/setup b/bin/setup
index cf8734b9785..bb38fe0124b 100755
--- a/bin/setup
+++ b/bin/setup
@@ -34,8 +34,6 @@ Dir.chdir APP_ROOT do
   run "cp keys/equifax_rsa.example keys/equifax_rsa"
   run "cp keys/equifax_rsa.pub.example keys/equifax_rsa.pub"
   run "cp certs/saml.crt.example certs/saml.crt"
-  run "gpg --dearmor < keys/equifax_gpg.pub.example > keys/equifax_gpg.pub.bin"
-  system "gpg --batch --import keys/equifax_gpg.example"
 
   if ARGV.shift == "--docker" then
     run 'docker-compose build'
@@ -45,6 +43,11 @@ Dir.chdir APP_ROOT do
     exit
   end
 
+  puts "\n== GPG Setup =="
+  run "which gpg || brew install gpg"
+  run "gpg --dearmor < keys/equifax_gpg.pub.example > keys/equifax_gpg.pub.bin"
+  system "gpg --batch --import keys/equifax_gpg.example"
+
   puts "\n== Installing dependencies =="
   run "gem install bundler --conservative"
   run 'gem install foreman --conservative && gem update foreman'