Date: Thu, 15 Nov 2018 18:48:26 -0500
Subject: [PATCH 36/39] LG-795 Display timestamps in the local timezone (#2654)
**Why**: As a user I want to see events in my account history in my local timezone
**How**: Since we do not store a user's timezone, update timestamps to the user's local timezone by adding client side javascript which uses the timezone of the browser. Use the local_time gem. Set the locale in the javascript by grabbing it from the path in the url. Add bug fixes and internationalization.
---
.rubocop.yml | 1 +
Gemfile | 1 +
Gemfile.lock | 2 +
app/decorators/event_decorator.rb | 2 +-
app/decorators/identity_decorator.rb | 8 +-
app/javascript/app/local-time.js | 108 +++++++++++++++++++
app/javascript/packs/application.js | 1 +
app/presenters/utc_time_presenter.rb | 2 +-
app/views/accounts/_connected_app.html.slim | 3 +-
app/views/accounts/_event_item.html.slim | 2 +-
app/views/accounts/_identity_item.html.slim | 2 +-
config/i18n-tasks.yml | 1 +
config/locales/account/en.yml | 2 +-
config/locales/account/es.yml | 2 +-
config/locales/account/fr.yml | 2 +-
spec/features/account_connected_apps_spec.rb | 3 +-
spec/presenters/utc_time_presenter_spec.rb | 2 +-
17 files changed, 131 insertions(+), 13 deletions(-)
create mode 100644 app/javascript/app/local-time.js
diff --git a/.rubocop.yml b/.rubocop.yml
index 11d3b732a5e..e962e136b39 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -36,6 +36,7 @@ Metrics/BlockLength:
Exclude:
- 'Rakefile'
- '**/*.rake'
+ - 'app/decorators/identity_decorator.rb'
- 'app/decorators/user_decorator.rb'
- 'app/services/omniauth_authorizer.rb'
- 'app/services/single_logout_handler.rb'
diff --git a/Gemfile b/Gemfile
index b113cf1ea60..a5bace60a8e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -25,6 +25,7 @@ gem 'http_accept_language'
gem 'httparty'
gem 'identity-hostdata', github: '18F/identity-hostdata', branch: 'master'
gem 'json-jwt'
+gem 'local_time'
gem 'lograge'
gem 'net-sftp'
gem 'newrelic_rpm'
diff --git a/Gemfile.lock b/Gemfile.lock
index 4e7be6be44c..92e177232b4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -333,6 +333,7 @@ GEM
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
+ local_time (2.1.0)
lograge (0.10.0)
actionpack (>= 4)
activesupport (>= 4)
@@ -707,6 +708,7 @@ DEPENDENCIES
json-jwt
knapsack
lexisnexis!
+ local_time
lograge
net-sftp
newrelic_rpm
diff --git a/app/decorators/event_decorator.rb b/app/decorators/event_decorator.rb
index 255747154e6..3937b08c368 100644
--- a/app/decorators/event_decorator.rb
+++ b/app/decorators/event_decorator.rb
@@ -8,7 +8,7 @@ def event_type
end
def happened_at
- event.created_at
+ event.created_at.utc
end
def happened_at_in_words
diff --git a/app/decorators/identity_decorator.rb b/app/decorators/identity_decorator.rb
index 63c65cc6eeb..15d9544be6a 100644
--- a/app/decorators/identity_decorator.rb
+++ b/app/decorators/identity_decorator.rb
@@ -19,11 +19,15 @@ def return_to_sp_url
end
def created_at_in_words
- UtcTimePresenter.new(identity.created_at).to_s
+ UtcTimePresenter.new(created_at).to_s
+ end
+
+ def created_at
+ identity.created_at.utc
end
def happened_at
- identity.last_authenticated_at
+ identity.last_authenticated_at.utc
end
def happened_at_in_words
diff --git a/app/javascript/app/local-time.js b/app/javascript/app/local-time.js
new file mode 100644
index 00000000000..f3689498693
--- /dev/null
+++ b/app/javascript/app/local-time.js
@@ -0,0 +1,108 @@
+// modifications marked with "login.gov" original here: https://github.com/basecamp/local_time/blob/master/app/assets/javascripts/local-time.js
+
+(function(){var t=this;(function(){(function(){var t=[].slice;
+
+// login.gov
+window.LocalTime={
+
+ config:{},run:function(){return this.getController().processElements()},process:function(){var e,n,r,a;for(n=1<=arguments.length?t.call(arguments,0):[],r=0,a=n.length;r11?"pm":"am")).toUpperCase();case"P":return i("time."+(c>11?"pm":"am"));case"S":return n(h,m);case"w":return u;case"y":return n(f%100,m);case"Y":return f;case"Z":return r(e)}})},n=function(t,e){switch(e){case"-":return t;default:return("0"+t).slice(-2)}},r=function(t){var e,n,r,a,i;return i=t.toString(),(e=null!=(n=i.match(/\(([\w\s]+)\)$/))?n[1]:void 0)?/\s/.test(e)?e.match(/\b(\w)/g).join(""):e:(e=null!=(r=i.match(/(\w{3,4})\s\d{4}$/))?r[1]:void 0)?e:(e=null!=(a=i.match(/(UTC[\+\-]\d+)/))?a[1]:void 0)?e:""}}.call(this),function(){e.CalendarDate=function(){function t(t,e,n){this.date=new Date(Date.UTC(t,e-1)),this.date.setUTCDate(n),this.year=this.date.getUTCFullYear(),this.month=this.date.getUTCMonth()+1,this.day=this.date.getUTCDate(),this.value=this.date.getTime()}return t.fromDate=function(t){return new this(t.getFullYear(),t.getMonth()+1,t.getDate())},t.today=function(){return this.fromDate(new Date)},t.prototype.equals=function(t){return(null!=t?t.value:void 0)===this.value},t.prototype.is=function(t){return this.equals(t)},t.prototype.isToday=function(){return this.is(this.constructor.today())},t.prototype.occursOnSameYearAs=function(t){return this.year===(null!=t?t.year:void 0)},t.prototype.occursThisYear=function(){return this.occursOnSameYearAs(this.constructor.today())},t.prototype.daysSince=function(t){if(t)return(this.date-t.date)/864e5},t.prototype.daysPassed=function(){return this.constructor.today().daysSince(this)},t}()}.call(this),function(){var t,n,r;n=e.strftime,r=e.translate,t=e.getI18nValue,e.RelativeTime=function(){function a(t){this.date=t,this.calendarDate=e.CalendarDate.fromDate(this.date)}return a.prototype.toString=function(){var t,e;return(e=this.toTimeElapsedString())?r("time.elapsed",{time:e}):(t=this.toWeekdayString())?(e=this.toTimeString(),r("datetime.at",{date:t,time:e})):r("date.on",{date:this.toDateString()})},a.prototype.toTimeOrDateString=function(){return this.calendarDate.isToday()?this.toTimeString():this.toDateString()},a.prototype.toTimeElapsedString=function(){var t,e,n,a,i;return n=(new Date).getTime()-this.date.getTime(),a=Math.round(n/1e3),e=Math.round(a/60),t=Math.round(e/60),n<0?null:a<10?(i=r("time.second"),r("time.singular",{time:i})):a<45?a+" "+r("time.seconds"):a<90?(i=r("time.minute"),r("time.singular",{time:i})):e<45?e+" "+r("time.minutes"):e<90?(i=r("time.hour"),r("time.singularAn",{time:i})):t<24?t+" "+r("time.hours"):""},a.prototype.toWeekdayString=function(){switch(this.calendarDate.daysPassed()){case 0:return r("date.today");case 1:return r("date.yesterday");case-1:return r("date.tomorrow");case 2:case 3:case 4:case 5:case 6:return n(this.date,"%A");default:return""}},a.prototype.toDateString=function(){var e;return e=t(this.calendarDate.occursThisYear()?"date.formats.thisYear":"date.formats.default"),n(this.date,e)},a.prototype.toTimeString=function(){return n(this.date,t("time.formats.default"))},a}()}.call(this),function(){var t,n=function(t,e){return function(){return t.apply(e,arguments)}};t=e.elementMatchesSelector,e.PageObserver=function(){function e(t,e){this.selector=t,this.callback=e,this.processInsertion=n(this.processInsertion,this),this.processMutations=n(this.processMutations,this)}return e.prototype.start=function(){if(!this.started)return this.observeWithMutationObserver()||this.observeWithMutationEvent(),this.started=!0},e.prototype.observeWithMutationObserver=function(){var t;if("undefined"!=typeof MutationObserver&&null!==MutationObserver)return t=new MutationObserver(this.processMutations),t.observe(document.documentElement,{childList:!0,subtree:!0}),!0},e.prototype.observeWithMutationEvent=function(){return addEventListener("DOMNodeInserted",this.processInsertion,!1),!0},e.prototype.findSignificantElements=function(e){var n;return n=[],(null!=e?e.nodeType:void 0)===Node.ELEMENT_NODE&&(t(e,this.selector)&&n.push(e),n.push.apply(n,e.querySelectorAll(this.selector))),n},e.prototype.processMutations=function(t){var e,n,r,a,i,o,s,u;for(e=[],n=0,a=t.length;n
Date: Fri, 16 Nov 2018 10:51:43 -0500
Subject: [PATCH 37/39] LG-583 Update account reset final delete screen design
(#2652)
**Why**: To make the delete option less prominent since the user will not be able to recover any information linked to their account.
**How**: Use the same format as the confirmation of delete screen at the beginning of the process.
---
.../account_reset/delete_account/show.html.slim | 12 +++++-------
config/locales/account_reset/en.yml | 2 --
config/locales/account_reset/es.yml | 2 --
config/locales/account_reset/fr.yml | 2 --
spec/features/account_reset/delete_account_spec.rb | 2 +-
.../delete_account/show.html.slim_spec.rb | 2 +-
6 files changed, 7 insertions(+), 15 deletions(-)
diff --git a/app/views/account_reset/delete_account/show.html.slim b/app/views/account_reset/delete_account/show.html.slim
index 0ad05055252..f74626f8f24 100644
--- a/app/views/account_reset/delete_account/show.html.slim
+++ b/app/views/account_reset/delete_account/show.html.slim
@@ -6,10 +6,8 @@ p.mt-tiny.mb0
br
h4.my2 = t('account_reset.delete_account.are_you_sure')
-= button_to t('account_reset.delete_account.delete_button'), \
- account_reset_delete_account_path, method: :delete, \
- class: 'btn btn-red col-6 p2 rounded-lg border bw2 bg-lightest-red border-red border-box'
-br
-br
-hr
-= link_to t('account_reset.delete_account.cancel'), root_url
+= button_to t('account_reset.request.no_cancel'), root_url, method: :get,
+ class: 'btn btn-primary btn-wide mb1 personal-key-continue',
+ 'data-toggle': 'modal'
+= button_to t('account_reset.request.yes_continue'), account_reset_delete_account_path, \
+ method: :delete, class: 'btn btn-link'
diff --git a/config/locales/account_reset/en.yml b/config/locales/account_reset/en.yml
index aa5ca0313a3..45d5f44a951 100644
--- a/config/locales/account_reset/en.yml
+++ b/config/locales/account_reset/en.yml
@@ -21,8 +21,6 @@ en:
phone number.
delete_account:
are_you_sure: Are you sure you want to delete your account?
- cancel: Cancel
- delete_button: Delete account
info: Deleting your account should be your last resort if you are locked out
of your account. You will not be able to recover any information linked to
your account. Once your account is deleted, you can create a new one using
diff --git a/config/locales/account_reset/es.yml b/config/locales/account_reset/es.yml
index ec756dff785..216599f027d 100644
--- a/config/locales/account_reset/es.yml
+++ b/config/locales/account_reset/es.yml
@@ -22,8 +22,6 @@ es:
a su registro número de teléfono.
delete_account:
are_you_sure: "¿Seguro que quieres eliminar tu cuenta?"
- cancel: Cancelar
- delete_button: Borrar cuenta
info: Eliminar su cuenta debe ser su último recurso si está bloqueado
de tu cuenta No podrá recuperar ninguna información vinculada a su cuenta.
Una vez que se elimine su cuenta, puede crear una nueva usando la misma dirección
diff --git a/config/locales/account_reset/fr.yml b/config/locales/account_reset/fr.yml
index bbd0758b6d5..12bf48555c0 100644
--- a/config/locales/account_reset/fr.yml
+++ b/config/locales/account_reset/fr.yml
@@ -23,8 +23,6 @@ fr:
votre numéro de téléphone enregistré.
delete_account:
are_you_sure: Êtes-vous sûr de vouloir supprimer votre compte?
- cancel: Annuler
- delete_button: Supprimer le compte
info: La suppression de votre compte devrait être votre dernier recours si vous
êtes en lock-out de votre compte Vous ne pourrez pas récupérer les informations
liées à ton compte. Une fois votre compte supprimé, vous pouvez en créer un
diff --git a/spec/features/account_reset/delete_account_spec.rb b/spec/features/account_reset/delete_account_spec.rb
index ef4d0461664..fbe44556a09 100644
--- a/spec/features/account_reset/delete_account_spec.rb
+++ b/spec/features/account_reset/delete_account_spec.rb
@@ -32,7 +32,7 @@
expect(page).to have_content(t('account_reset.delete_account.title'))
expect(page).to have_current_path(account_reset_delete_account_path)
- click_on t('account_reset.delete_account.delete_button')
+ click_button t('account_reset.request.yes_continue')
expect(page).to have_content(
strip_tags(
diff --git a/spec/views/account_reset/delete_account/show.html.slim_spec.rb b/spec/views/account_reset/delete_account/show.html.slim_spec.rb
index bd5573e0aba..eb22b80c76b 100644
--- a/spec/views/account_reset/delete_account/show.html.slim_spec.rb
+++ b/spec/views/account_reset/delete_account/show.html.slim_spec.rb
@@ -13,6 +13,6 @@
it 'has button to delete' do
render
- expect(rendered).to have_button t('account_reset.delete_account.delete_button')
+ expect(rendered).to have_button t('account_reset.request.yes_continue')
end
end
From 8c2d6ec3d42d57093b5ebb81f99176e8dc848ae2 Mon Sep 17 00:00:00 2001
From: Jonathan Hooper
Date: Fri, 16 Nov 2018 15:15:28 -0600
Subject: [PATCH 38/39] LG-785 Alert users when personal key is regenerated
(#2660)
**Why**: So that user's can be aware of any potential suspicious
activity associated with their account.
---
.../users/personal_keys_controller.rb | 10 +++++++
..._personal_key_regeneration_notifier_job.rb | 14 +++++++++
app/mailers/user_mailer.rb | 4 +++
.../personal_key_regenerated.html.slim | 16 ++++++++++
config/locales/jobs/en.yml | 7 +++--
config/locales/jobs/es.yml | 5 +++-
config/locales/jobs/fr.yml | 6 +++-
config/locales/user_mailer/en.yml | 26 +++++++++++++---
config/locales/user_mailer/es.yml | 21 +++++++++++++
config/locales/user_mailer/fr.yml | 21 +++++++++++++
.../users/regenerate_personal_key_spec.rb | 19 ++++++++++--
...onal_key_regeneration_notifier_job_spec.rb | 30 +++++++++++++++++++
spec/mailers/user_mailer_spec.rb | 20 +++++++++++++
13 files changed, 189 insertions(+), 10 deletions(-)
create mode 100644 app/jobs/sms_personal_key_regeneration_notifier_job.rb
create mode 100644 app/views/user_mailer/personal_key_regenerated.html.slim
create mode 100644 spec/jobs/sms_personal_key_regeneration_notifier_job_spec.rb
diff --git a/app/controllers/users/personal_keys_controller.rb b/app/controllers/users/personal_keys_controller.rb
index 72f2b13d462..4dfccebce70 100644
--- a/app/controllers/users/personal_keys_controller.rb
+++ b/app/controllers/users/personal_keys_controller.rb
@@ -27,6 +27,7 @@ def create
user_session[:personal_key] = create_new_code
analytics.track_event(Analytics::PROFILE_PERSONAL_KEY_CREATE)
Event.create(user_id: current_user.id, event_type: :new_personal_key)
+ send_new_personal_key_notification
redirect_to manage_personal_key_url
end
@@ -45,5 +46,14 @@ def next_step
def user_has_not_visited_any_sp_yet?
current_user.identities.pluck(:last_authenticated_at).compact.empty?
end
+
+ def send_new_personal_key_notification
+ current_user.confirmed_email_addresses.each do |email_address|
+ UserMailer.personal_key_regenerated(email_address.email).deliver_now
+ end
+ MfaContext.new(current_user).phone_configurations.each do |phone_configuration|
+ SmsPersonalKeyRegenerationNotifierJob.perform_now(phone: phone_configuration.phone)
+ end
+ end
end
end
diff --git a/app/jobs/sms_personal_key_regeneration_notifier_job.rb b/app/jobs/sms_personal_key_regeneration_notifier_job.rb
new file mode 100644
index 00000000000..3ac8db56757
--- /dev/null
+++ b/app/jobs/sms_personal_key_regeneration_notifier_job.rb
@@ -0,0 +1,14 @@
+class SmsPersonalKeyRegenerationNotifierJob < ApplicationJob
+ queue_as :sms
+
+ # :reek:UtilityFunction
+ def perform(phone:)
+ TwilioService::Utils.new.send_sms(
+ to: phone,
+ body: I18n.t(
+ 'jobs.sms_personal_key_regeneration_notifier_job.message',
+ app: APP_NAME
+ )
+ )
+ end
+end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index dcf0044e6a0..f6461808faa 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -31,6 +31,10 @@ def personal_key_sign_in(email)
mail(to: email, subject: t('user_mailer.personal_key_sign_in.subject'))
end
+ def personal_key_regenerated(email)
+ mail(to: email, subject: t('user_mailer.personal_key_regenerated.subject'))
+ end
+
def account_reset_request(email_address, account_reset)
@token = account_reset&.request_token
mail(to: email_address.email, subject: t('user_mailer.account_reset_request.subject'))
diff --git a/app/views/user_mailer/personal_key_regenerated.html.slim b/app/views/user_mailer/personal_key_regenerated.html.slim
new file mode 100644
index 00000000000..9ae7e2becc0
--- /dev/null
+++ b/app/views/user_mailer/personal_key_regenerated.html.slim
@@ -0,0 +1,16 @@
+p.lead
+ strong == t('.intro')
+
+table.spacer
+ tbody
+ tr
+ td.s10 height="10px"
+ |
+table.hr
+ tr
+ th
+ |
+
+p == t('.help_html',
+ reset_password_url: forgot_password_url,
+ account_url: account_url)
diff --git a/config/locales/jobs/en.yml b/config/locales/jobs/en.yml
index 864b89c864a..f74ba0a79ba 100644
--- a/config/locales/jobs/en.yml
+++ b/config/locales/jobs/en.yml
@@ -12,6 +12,9 @@ en:
in to your account. This code will expire in %{expiration} minutes."
verify_message: "%{code} is your %{app} confirmation code. Use this to confirm
your phone number. This code will expire in %{expiration} minutes."
+ sms_personal_key_regeneration_notifier_job:
+ message: A new personal key has been issued for your %{app} account. If this
+ wasn’t you, reset your password and contact us at security@login.gov.
sms_personal_key_sign_in_notifier_job:
- message: Your personal key was just used to sign into your login.gov account.
- If this wasn’t you, reset your password and contact us at security@login.gov.
+ message: Your personal key was just used to sign into your %{app} account. If
+ this wasn’t you, reset your password and contact us at security@login.gov.
diff --git a/config/locales/jobs/es.yml b/config/locales/jobs/es.yml
index d2aec114d8d..8d29586838d 100644
--- a/config/locales/jobs/es.yml
+++ b/config/locales/jobs/es.yml
@@ -12,6 +12,9 @@ es:
ingresando a su cuenta. Este código caducará en %{expiration} minutos."
verify_message: "%{code} es tu código de confirmación de %{app}. Use esto para
confirmar su número de teléfono. Este código caducará en %{expiration} minutos."
+ sms_personal_key_regeneration_notifier_job:
+ message: Se ha emitido una nueva clave personal para tu cuenta %{app}. Si no
+ eres tú, restablece tu contraseña y ponte en contacto con nosotros en security@login.gov.
sms_personal_key_sign_in_notifier_job:
message: Su clave personal solo se utilizó para iniciar sesión en su cuenta
- login.gov. Si no fue así, reinicie su contraseña y contáctenos en security@login.gov.
+ %{app}. Si no fue así, reinicie su contraseña y contáctenos en security@login.gov.
diff --git a/config/locales/jobs/fr.yml b/config/locales/jobs/fr.yml
index a2aa952d6ab..f4302b8bc74 100644
--- a/config/locales/jobs/fr.yml
+++ b/config/locales/jobs/fr.yml
@@ -14,7 +14,11 @@ fr:
verify_message: "%{code} est votre code de confirmation %{app}. Utilisez-le
pour confirmer votre numéro de téléphone. Ce code expirera dans %{expiration}
minutes."
+ sms_personal_key_regeneration_notifier_job:
+ message: Une nouvelle clé personnelle a été émise pour votre compte %{app}.
+ Si vous ne l'avez pas demandée, réinitialisez votre mot de passe et contactez-nous
+ à security@login.gov.
sms_personal_key_sign_in_notifier_job:
message: Votre clé personnelle a été utilisée pour vous connecter à votre compte
- login.gov. Si ce n’était pas vous, changez votre mot de passe et contactez-nous
+ %{app}. Si ce n’était pas vous, changez votre mot de passe et contactez-nous
à security@login.gov.
diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml
index aaf2a4491e0..55f0e59a478 100644
--- a/config/locales/user_mailer/en.yml
+++ b/config/locales/user_mailer/en.yml
@@ -44,6 +44,24 @@ en:
help: If you did not make this change, please visit the %{app} %{help_link}
or %{contact_link}.
intro: You have a new password for your %{app} account.
+ personal_key_regenerated:
+ help_html: Your login.gov account was just issued a new 16-character personal
+ key. You're getting this email to make sure it was you.
If you just
+ signed in and regenerated your personal key, great! There's nothing you need
+ to do.
If you did not just regenerate your personal key, or if you're
+ not sure, please immediately take these steps to secure your account:
- Change your password. Choose a password
+ that you haven't already used with this account.
- Sign
+ in to your login.gov account and make sure you recognize all
+ of the information on your account page, including the methods you use for
+ two-factor authentication, such as phone number, authentication app, or security
+ key.
- On your login.gov account page,
+ request a new personal key. Remember, never share it unless you are
+ using it to sign into a trusted website that uses login.gov.
You
+ should then contact us by calling 844-875-6446 or emailing security@login.gov.
Thanks,
The
+ login.gov team
+ intro: New personal key issued
+ subject: Account Security Alert
personal_key_sign_in:
help_html: Your login.gov account was just signed into using your 16-character
personal key. You're getting this email to make sure it was you.
If
@@ -55,10 +73,10 @@ en:
in to your login.gov account
and make sure you recognize all
of the information on your account page, including the methods you use for
two-factor authentication, such as phone number, authentication app, or security
- key.On your login.gov account page, request a new personal
- key. Remember, never share it unless you are using it to sign into
- a trusted website that uses login.gov.You should then contact
- us by calling 844-875-6446 or emailing security@login.gov.
+ key.On your login.gov account page,
+ request a new personal key. Remember, never share it unless you are
+ using it to sign into a trusted website that uses login.gov.You
+ should then contact us by calling 844-875-6446 or emailing security@login.gov.
Thanks,
The login.gov team
intro: Personal key used to sign in
subject: Account Security Alert
diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml
index eb4eb6a58dc..5cc4f6ba901 100644
--- a/config/locales/user_mailer/es.yml
+++ b/config/locales/user_mailer/es.yml
@@ -43,6 +43,27 @@ es:
password_changed:
help: Si no realizó este cambio, visite el %{app} %{help_link} o el %{contact_link}.
intro: Tiene una contraseña nueva para su cuenta de %{app}.
+ personal_key_regenerated:
+ help_html: Tu cuenta de login.gov acaba de emitir una nueva clave personal
+ de 16 caracteres. Estás recibiendo este correo electrónico para verificar
+ que eras tú.
Si acabas de iniciar sesión y has regenerado tu clave
+ personal, ¡fantástico! No es necesario que hagas nada.
Si no acabas
+ de regenerar tu clave personal, o si no estás seguro, sigue estos pasos para
+ proteger tu cuenta:
- Cambia
+ tu contraseña. Elige una contraseña que aún no hayas utilizado
+ con esta cuenta.
- Inicia sesión en
+ tu cuenta de login.gov y asegúrate de que reconoces toda la información
+ de la página de tu cuenta, como los métodos que utilizas para la autenticación
+ de dos factores, como el número de teléfono, la aplicación de autenticación
+ o la clave de seguridad.
- En la página
+ de tu cuenta de login.gov, solicita una nueva clave personal.
+ Recuerda no compartirla nunca a menos que la estés usando para acceder a un
+ sitio web de confianza que utilice login.gov.
Deberías ponerte
+ en contacto con nosotros llamando al 844-875-6446 o enviando un correo electrónico
+ a security@login.gov.
Gracias,
El
+ equipo de login.gov
+ intro: Nueva clave personal emitida
+ subject: Alerta de seguridad de la cuenta
personal_key_sign_in:
help_html: Su cuenta login.gov acaba de iniciar sesión con su clave personal
de 16 caracteres. Usted está recibiendo este e-mail para asegurarse de que
diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml
index 1c386e50dba..5e6e73b709c 100644
--- a/config/locales/user_mailer/fr.yml
+++ b/config/locales/user_mailer/fr.yml
@@ -45,6 +45,27 @@ fr:
help: Si vous n'avez pas changé votre mot de passe, veuillez visiter le %{help_link}
de %{app} ou %{contact_link}.
intro: Le mot de passe de votre compte %{app} a été changé.
+ personal_key_regenerated:
+ help_html:
Votre compte login.gov vient de recevoir une nouvelle clé personnelle
+ de 16 caractères. Le but de cet e-mail est de s'assurer que c'est bien vous
+ qui en êtes à l'origine.
Si vous venez de vous connecter et de régénérer
+ votre clé personnelle, c'est parfait ! Vous n'avez rien à faire.
Si
+ vous ne venez pas de régénérer votre clé personnelle, ou en cas de doute,
+ effectuez immédiatement les actions suivantes pour sécuriser votre compte :
- Modifiez votre mot de passe. Choisissez
+ un mot de passe que vous n'avez pas encore utilisé avec ce compte.
- Connectez-vous à votre compte login.gov
+ et vérifiez bien que toutes les informations sur la page de votre compte sont
+ correctes, y compris les méthodes que vous utilisez pour l’authentification
+ à deux facteurs, dont le numéro de téléphone, l’application d’authentification
+ ou la clé de sécurité.
- Sur votre page
+ de compte login.gov, demandez une nouvelle clé personnelle. N'oubliez
+ pas de ne jamais la partager, sauf si vous l'utilisez pour vous connecter
+ à un site de confiance qui utilise login.gov.
Veuillez ensuite
+ nous contacter en appelant le 844-875-6446 ou par e-mail à security@login.gov.
Merci,
L'équipe
+ login.gov
+ intro: Nouvelle clé personnelle émise
+ subject: Alerte de sécurité du compte
personal_key_sign_in:
help_html: Votre compte login.gov a été connecté à l'aide de votre clé personnelle.
Vous recevez cet email pour vous assurer que c'était bien vous.
Si vous
diff --git a/spec/features/users/regenerate_personal_key_spec.rb b/spec/features/users/regenerate_personal_key_spec.rb
index 88f9887669c..1bceac55cd6 100644
--- a/spec/features/users/regenerate_personal_key_spec.rb
+++ b/spec/features/users/regenerate_personal_key_spec.rb
@@ -5,12 +5,18 @@
include PersonalKeyHelper
include SamlAuthHelper
+ before { stub_twilio_service }
+
context 'during sign up' do
- scenario 'user refreshes personal key page' do
+ scenario 'refreshing personal key page displays the same key and does not notify the user' do
sign_up_and_view_personal_key
personal_key = scrape_personal_key
+ # The user should not receive an SMS and an email
+ expect(UserMailer).to_not receive(:personal_key_regenerated)
+ expect(SmsPersonalKeyRegenerationNotifierJob).to_not receive(:perform_now)
+
visit sign_up_personal_key_path
expect(current_path).to eq(sign_up_personal_key_path)
@@ -24,10 +30,19 @@
context 'after sign up' do
context 'regenerating personal key' do
- scenario 'displays new code' do
+ scenario 'displays new code and notifies the user' do
user = sign_in_and_2fa_user
old_digest = user.encrypted_recovery_code_digest
+ # The user should receive an SMS and an email
+ personal_key_sign_in_mail = double
+ expect(personal_key_sign_in_mail).to receive(:deliver_now)
+ expect(UserMailer).to receive(:personal_key_regenerated).
+ with(user.email).
+ and_return(personal_key_sign_in_mail)
+ expect(SmsPersonalKeyRegenerationNotifierJob).to receive(:perform_now).
+ with(phone: user.phone_configurations.first.phone)
+
click_button t('account.links.regenerate_personal_key')
expect(user.reload.encrypted_recovery_code_digest).to_not eq old_digest
diff --git a/spec/jobs/sms_personal_key_regeneration_notifier_job_spec.rb b/spec/jobs/sms_personal_key_regeneration_notifier_job_spec.rb
new file mode 100644
index 00000000000..68a1763527b
--- /dev/null
+++ b/spec/jobs/sms_personal_key_regeneration_notifier_job_spec.rb
@@ -0,0 +1,30 @@
+require 'rails_helper'
+
+describe SmsPersonalKeyRegenerationNotifierJob do
+ include Features::ActiveJobHelper
+
+ before do
+ reset_job_queues
+ TwilioService::Utils.telephony_service = FakeSms
+ FakeSms.messages = []
+ end
+
+ describe '.perform' do
+ it 'sends a message about the personal key sign in to the user' do
+ allow(Figaro.env).to receive(:twilio_messaging_service_sid).and_return('fake_sid')
+
+ described_class.perform_now(phone: '+1 (202) 345-6789')
+
+ messages = FakeSms.messages
+
+ expect(messages.size).to eq(1)
+
+ msg = messages.first
+
+ expect(msg.messaging_service_sid).to eq('fake_sid')
+ expect(msg.to).to eq('+1 (202) 345-6789')
+ expect(msg.body).
+ to eq(I18n.t('jobs.sms_personal_key_regeneration_notifier_job.message', app: APP_NAME))
+ end
+ end
+end
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index 2ca18021e43..91a91a4fb4d 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -66,6 +66,26 @@
end
end
+ describe 'personal_key_regenerated' do
+ let(:mail) { UserMailer.personal_key_regenerated(user.email) }
+
+ it_behaves_like 'a system email'
+
+ it 'sends to the current email' do
+ expect(mail.to).to eq [user.email]
+ end
+
+ it 'renders the subject' do
+ expect(mail.subject).to eq t('user_mailer.personal_key_regenerated.subject')
+ end
+
+ it 'renders the body' do
+ expect(mail.html_part.body).to have_content(
+ t('user_mailer.personal_key_regenerated.intro')
+ )
+ end
+ end
+
describe 'signup_with_your_email' do
let(:mail) { UserMailer.signup_with_your_email(user.email) }
From e265d2dd1b272c08866a03e98bd276bcee0ec905 Mon Sep 17 00:00:00 2001
From: Steve Urciuoli
Date: Mon, 19 Nov 2018 14:31:38 -0500
Subject: [PATCH 39/39] LG-575 Add a phone (#2662)
**Why**: We want the user to be able to add multiple phone numbers to their account without replacing any already configured.
**How**: Replace the generic user.phone_configurations.first, which assumed one phone, with specific phone ids. Pass these ids when managing, updating, and authenticating. Create a new screen for adding a phone fashioned after the existing edit phone screen.
---
.../concerns/phone_confirmation.rb | 8 ++---
.../concerns/two_factor_authenticatable.rb | 22 +++++++-----
.../options_controller.rb | 7 +++-
.../otp_verification_controller.rb | 2 +-
.../users/phone_setup_controller.rb | 2 +-
app/controllers/users/phones_controller.rb | 36 ++++++++++++++++---
.../two_factor_authentication_controller.rb | 5 +--
app/decorators/mfa_context.rb | 5 +++
app/forms/two_factor_login_options_form.rb | 16 ++++++---
app/forms/two_factor_options_form.rb | 2 +-
.../otp_delivery_preference_updater.rb | 8 ++---
app/services/update_user.rb | 12 ++++---
app/views/accounts/_phone.html.slim | 7 ++--
app/views/users/phones/add.html.slim | 23 ++++++++++++
app/views/users/phones/edit.html.slim | 2 +-
config/locales/headings/en.yml | 2 ++
config/locales/headings/es.yml | 2 ++
config/locales/headings/fr.yml | 2 ++
config/locales/titles/en.yml | 2 ++
config/locales/titles/es.yml | 2 ++
config/locales/titles/fr.yml | 2 ++
config/routes.rb | 2 ++
.../otp_verification_controller_spec.rb | 2 ++
.../users/phones_controller_spec.rb | 31 ++++++++++++++++
.../change_factor_spec.rb | 2 +-
spec/features/users/sign_in_spec.rb | 2 +-
.../otp_delivery_preference_updater_spec.rb | 6 ++--
spec/services/update_user_spec.rb | 15 --------
spec/views/accounts/show.html.slim_spec.rb | 12 +++----
29 files changed, 174 insertions(+), 67 deletions(-)
create mode 100644 app/views/users/phones/add.html.slim
diff --git a/app/controllers/concerns/phone_confirmation.rb b/app/controllers/concerns/phone_confirmation.rb
index ebf8df79f07..266a59df7f6 100644
--- a/app/controllers/concerns/phone_confirmation.rb
+++ b/app/controllers/concerns/phone_confirmation.rb
@@ -1,21 +1,21 @@
module PhoneConfirmation
- def prompt_to_confirm_phone(phone:, selected_delivery_method: nil)
+ def prompt_to_confirm_phone(id:, phone:, selected_delivery_method: nil)
user_session[:unconfirmed_phone] = phone
user_session[:context] = 'confirmation'
redirect_to otp_send_url(
otp_delivery_selection_form: {
- otp_delivery_preference: otp_delivery_method(phone, selected_delivery_method),
+ otp_delivery_preference: otp_delivery_method(id, phone, selected_delivery_method),
}
)
end
private
- def otp_delivery_method(phone, selected_delivery_method)
+ def otp_delivery_method(id, phone, selected_delivery_method)
return :sms if PhoneNumberCapabilities.new(phone).sms_only?
return selected_delivery_method if selected_delivery_method.present?
- MfaContext.new(current_user).phone_configurations.first&.delivery_preference ||
+ MfaContext.new(current_user).phone_configuration(id)&.delivery_preference ||
current_user.otp_delivery_preference
end
end
diff --git a/app/controllers/concerns/two_factor_authenticatable.rb b/app/controllers/concerns/two_factor_authenticatable.rb
index 52df8a0072f..6478ebfa0ce 100644
--- a/app/controllers/concerns/two_factor_authenticatable.rb
+++ b/app/controllers/concerns/two_factor_authenticatable.rb
@@ -131,7 +131,7 @@ def handle_valid_otp_for_authentication_context
end
def assign_phone
- @updating_existing_number = old_phone.present?
+ @updating_existing_number = user_session[:phone_id].present?
if @updating_existing_number && confirmation_context?
phone_changed
@@ -142,10 +142,6 @@ def assign_phone
update_phone_attributes
end
- def old_phone
- MfaContext.new(current_user).phone_configurations.first&.phone
- end
-
def phone_changed
create_user_event(:phone_changed)
current_user.confirmed_email_addresses.each do |email_address|
@@ -160,7 +156,8 @@ def phone_confirmed
def update_phone_attributes
UpdateUser.new(
user: current_user,
- attributes: { phone: user_session[:unconfirmed_phone], phone_confirmed_at: Time.zone.now }
+ attributes: { phone_id: user_session[:phone_id], phone: user_session[:unconfirmed_phone],
+ phone_confirmed_at: Time.zone.now }
).call
end
@@ -253,7 +250,7 @@ def generic_data
def display_phone_to_deliver_to
if authentication_context?
- decorated_user.masked_two_factor_phone_number
+ masked_number(phone_configuration.phone)
else
user_session[:unconfirmed_phone]
end
@@ -261,7 +258,7 @@ def display_phone_to_deliver_to
def voice_otp_delivery_unsupported?
phone_number = if authentication_context?
- MfaContext.new(current_user).phone_configurations.first&.phone
+ phone_configuration&.phone
else
user_session[:unconfirmed_phone]
end
@@ -297,4 +294,13 @@ def presenter_for_two_factor_authentication_method
view: view_context
)
end
+
+ def phone_configuration
+ MfaContext.new(current_user).phone_configuration(user_session[:phone_id])
+ end
+
+ def masked_number(number)
+ return '' if number.blank?
+ "***-***-#{number[-4..-1]}"
+ end
end
diff --git a/app/controllers/two_factor_authentication/options_controller.rb b/app/controllers/two_factor_authentication/options_controller.rb
index 60a5c9080c9..bba4b0fc10b 100644
--- a/app/controllers/two_factor_authentication/options_controller.rb
+++ b/app/controllers/two_factor_authentication/options_controller.rb
@@ -55,8 +55,13 @@ def mfa_redirect_url
options = EXTRA_URL_OPTIONS[selection] || {}
configuration_id = @two_factor_options_form.configuration_id
- options[:id] = configuration_id if configuration_id.present?
+ user_session[:phone_id] = configuration_id if configuration_id.present?
+ options[:id] = user_session[:phone_id]
+ build_url(selection, options)
+ end
+
+ def build_url(selection, options)
method = FACTOR_TO_URL_METHOD[selection]
public_send(method, options) if method.present?
end
diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb
index 0de0813df51..e248d9c117c 100644
--- a/app/controllers/two_factor_authentication/otp_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb
@@ -58,7 +58,7 @@ def confirm_voice_capability
end
def phone
- MfaContext.new(current_user).phone_configurations.first&.phone ||
+ MfaContext.new(current_user).phone_configuration(user_session[:phone_id])&.phone ||
user_session[:unconfirmed_phone]
end
diff --git a/app/controllers/users/phone_setup_controller.rb b/app/controllers/users/phone_setup_controller.rb
index fdd95c94adf..574852e5ccd 100644
--- a/app/controllers/users/phone_setup_controller.rb
+++ b/app/controllers/users/phone_setup_controller.rb
@@ -21,7 +21,7 @@ def create
analytics.track_event(Analytics::MULTI_FACTOR_AUTH_PHONE_SETUP, result.to_h)
if result.success?
- prompt_to_confirm_phone(phone: @user_phone_form.phone)
+ prompt_to_confirm_phone(id: nil, phone: @user_phone_form.phone)
else
render :index
end
diff --git a/app/controllers/users/phones_controller.rb b/app/controllers/users/phones_controller.rb
index f32bcd2bce7..fa434197a11 100644
--- a/app/controllers/users/phones_controller.rb
+++ b/app/controllers/users/phones_controller.rb
@@ -4,7 +4,25 @@ class PhonesController < ReauthnRequiredController
before_action :confirm_two_factor_authenticated
+ def add
+ user_session[:phone_id] = nil
+ @user_phone_form = UserPhoneForm.new(current_user, nil)
+ @presenter = PhoneSetupPresenter.new(current_user.otp_delivery_preference)
+ end
+
+ def create
+ @user_phone_form = UserPhoneForm.new(current_user, nil)
+ @presenter = PhoneSetupPresenter.new(current_user.otp_delivery_preference)
+ if @user_phone_form.submit(user_params).success?
+ confirm_phone
+ bypass_sign_in current_user
+ else
+ render :add
+ end
+ end
+
def edit
+ set_phone_id
@user_phone_form = UserPhoneForm.new(current_user, phone_configuration)
@presenter = PhoneSetupPresenter.new(delivery_preference)
end
@@ -40,7 +58,7 @@ def delete
# doing away with this controller. Once we move to multiple phones, we'll allow
# adding and deleting, but not editing.
def phone_configuration
- MfaContext.new(current_user).phone_configurations.first
+ MfaContext.new(current_user).phone_configuration(user_session[:phone_id])
end
def user_params
@@ -55,18 +73,26 @@ def process_updates
form = @user_phone_form
if form.phone_changed?
analytics.track_event(Analytics::PHONE_CHANGE_REQUESTED)
- flash[:notice] = t('devise.registrations.phone_update_needs_confirmation')
- prompt_to_confirm_phone(
- phone: form.phone, selected_delivery_method: form.otp_delivery_preference
- )
+ confirm_phone
else
redirect_to account_url
end
end
+ def confirm_phone
+ flash[:notice] = t('devise.registrations.phone_update_needs_confirmation')
+ prompt_to_confirm_phone(id: user_session[:phone_id], phone: @user_phone_form.phone,
+ selected_delivery_method: @user_phone_form.otp_delivery_preference)
+ end
+
def handle_successful_delete
flash[:success] = t('two_factor_authentication.phone.delete.success')
create_user_event(:phone_removed)
end
+
+ def set_phone_id
+ phone_id = params[:id]
+ user_session[:phone_id] = phone_id if phone_id.present?
+ end
end
end
diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb
index 89f31c2a2eb..988331434f8 100644
--- a/app/controllers/users/two_factor_authentication_controller.rb
+++ b/app/controllers/users/two_factor_authentication_controller.rb
@@ -31,7 +31,7 @@ def phone_enabled?
end
def phone_configuration
- MfaContext.new(current_user).phone_configurations.first
+ MfaContext.new(current_user).phone_configuration(user_session[:phone_id])
end
def validate_otp_delivery_preference_and_send_code
@@ -51,10 +51,11 @@ def delivery_preference
end
def update_otp_delivery_preference_if_needed
+ return unless user_signed_in?
OtpDeliveryPreferenceUpdater.new(
user: current_user,
preference: delivery_params[:otp_delivery_preference],
- context: otp_delivery_selection_form.context
+ phone_id: user_session[:phone_id]
).call
end
diff --git a/app/decorators/mfa_context.rb b/app/decorators/mfa_context.rb
index 4f1f9042559..4749a42a093 100644
--- a/app/decorators/mfa_context.rb
+++ b/app/decorators/mfa_context.rb
@@ -13,6 +13,11 @@ def phone_configurations
end
end
+ def phone_configuration(id = nil)
+ return phone_configurations.first if id.blank?
+ phone_configurations.find { |cfg| cfg.id.to_s == id.to_s }
+ end
+
def webauthn_configurations
if user.present?
user.webauthn_configurations
diff --git a/app/forms/two_factor_login_options_form.rb b/app/forms/two_factor_login_options_form.rb
index c3d324b404a..3002a759a8d 100644
--- a/app/forms/two_factor_login_options_form.rb
+++ b/app/forms/two_factor_login_options_form.rb
@@ -11,11 +11,7 @@ def initialize(user)
end
def submit(params)
- selection = params[:selection]
- (selection, configuration_id) = selection.split(':', 2) if selection.present?
-
- self.selection = selection
- self.configuration_id = configuration_id
+ self.selection, self.configuration_id = selection_and_configuration_id(params)
success = valid?
@@ -28,6 +24,16 @@ def submit(params)
attr_writer :selection
attr_writer :configuration_id
+ def selection_and_configuration_id(params)
+ selection = params[:selection]
+ configuration_id = nil
+ if selection =~ /(.+)[:_](\d+)/
+ selection = Regexp.last_match(1)
+ configuration_id = Regexp.last_match(2)
+ end
+ [selection, configuration_id]
+ end
+
def extra_analytics_attributes
{
selection: selection,
diff --git a/app/forms/two_factor_options_form.rb b/app/forms/two_factor_options_form.rb
index 07be228f3e0..4c7e2b5bd9f 100644
--- a/app/forms/two_factor_options_form.rb
+++ b/app/forms/two_factor_options_form.rb
@@ -37,7 +37,7 @@ def extra_analytics_attributes
def user_needs_updating?
%w[voice sms].include?(selection) &&
- selection != MfaContext.new(user).phone_configurations.first&.delivery_preference &&
+ selection != MfaContext.new(user).phone_configuration&.delivery_preference &&
selection != user.otp_delivery_preference
end
diff --git a/app/services/otp_delivery_preference_updater.rb b/app/services/otp_delivery_preference_updater.rb
index ce1592a504d..bead12dc960 100644
--- a/app/services/otp_delivery_preference_updater.rb
+++ b/app/services/otp_delivery_preference_updater.rb
@@ -1,8 +1,8 @@
class OtpDeliveryPreferenceUpdater
- def initialize(user:, preference:, context:)
+ def initialize(user:, preference:, phone_id:)
@user = user
@preference = preference
- @context = context
+ @phone_id = phone_id
end
def call
@@ -12,7 +12,7 @@ def call
private
- attr_reader :user, :preference, :context
+ attr_reader :user, :preference, :phone_id
def should_update_user?
return false unless user
@@ -21,7 +21,7 @@ def should_update_user?
def otp_delivery_preference_changed?
return true if preference != user.otp_delivery_preference
- phone_configuration = MfaContext.new(user).phone_configurations.first
+ phone_configuration = MfaContext.new(user).phone_configuration(phone_id)
phone_configuration.present? && preference != phone_configuration.delivery_preference
end
end
diff --git a/app/services/update_user.rb b/app/services/update_user.rb
index 014fa8e8277..a1cef214fae 100644
--- a/app/services/update_user.rb
+++ b/app/services/update_user.rb
@@ -5,7 +5,7 @@ def initialize(user:, attributes:)
end
def call
- result = user.update!(attributes.except(:phone, :phone_confirmed_at))
+ result = user.update!(attributes.except(:phone_id, :phone, :phone_confirmed_at))
manage_phone_configuration
result
end
@@ -15,7 +15,7 @@ def call
attr_reader :user, :attributes
def manage_phone_configuration
- if MfaContext.new(user).phone_configurations.any?
+ if attributes[:phone_id].present?
update_phone_configuration
else
create_phone_configuration
@@ -23,14 +23,18 @@ def manage_phone_configuration
end
def update_phone_configuration
- MfaContext.new(user).phone_configurations.first.update!(phone_attributes)
+ MfaContext.new(user).phone_configuration(attributes[:phone_id]).update!(phone_attributes)
end
def create_phone_configuration
- return if phone_attributes[:phone].blank?
+ return if phone_attributes[:phone].blank? || duplicate_phone?
MfaContext.new(user).phone_configurations.create(phone_attributes)
end
+ def duplicate_phone?
+ MfaContext.new(user).phone_configurations.map(&:phone).index(phone_attributes[:phone])
+ end
+
def phone_attributes
@phone_attributes ||= {
phone: attributes[:phone],
diff --git a/app/views/accounts/_phone.html.slim b/app/views/accounts/_phone.html.slim
index a159b29c771..3a8c23ead90 100644
--- a/app/views/accounts/_phone.html.slim
+++ b/app/views/accounts/_phone.html.slim
@@ -3,14 +3,13 @@
.col.col-6.bold
= t('account.index.phone')
.right-align.col.col-6
- - if MfaContext.new(current_user).phone_configurations.empty?
- .btn.btn-account-action.rounded-lg.bg-light-blue
- = link_to t('account.index.phone_add'), manage_phone_path
+ .btn.btn-account-action.rounded-lg.bg-light-blue
+ = link_to t('account.index.phone_add'), add_phone_path
- MfaContext.new(current_user).phone_configurations.each do |phone_configuration|
.p2.col.col-12.border-top.border-blue-light.account-list-item
.col.col-8.sm-6.truncate
= phone_configuration.phone
.col.col-4.sm-6.right-align
= render @view_model.manage_action_partial,
- path: manage_phone_url,
+ path: manage_phone_url(id: phone_configuration.id),
name: t('account.index.phone')
diff --git a/app/views/users/phones/add.html.slim b/app/views/users/phones/add.html.slim
new file mode 100644
index 00000000000..6ad88eb65ea
--- /dev/null
+++ b/app/views/users/phones/add.html.slim
@@ -0,0 +1,23 @@
+- title t('titles.add_info.phone')
+
+h1.h3.my0 = t('headings.add_info.phone')
+= simple_form_for(@user_phone_form,
+ html: { autocomplete: 'off', method: :post, role: 'form' },
+ data: { international_phone_form: true },
+ url: add_phone_path) do |f|
+ .sm-col-8.js-intl-tel-code-select
+ = f.input :international_code,
+ collection: international_phone_codes,
+ include_blank: false,
+ input_html: { class: 'international-code' }
+ .sm-col-8.mb3
+ = f.label :phone
+ strong.left = @presenter.label
+ = f.input :phone, as: :tel, label: false, required: true,
+ input_html: { class: 'phone col-8 mb4' }
+ = render 'users/shared/otp_delivery_preference_selection'
+ = f.button :submit, t('forms.buttons.continue'), class: 'btn-wide'
+= render 'shared/cancel', link: account_path
+
+= stylesheet_link_tag 'intl-tel-number/intlTelInput'
+= javascript_pack_tag 'intl-tel-input'
diff --git a/app/views/users/phones/edit.html.slim b/app/views/users/phones/edit.html.slim
index 58e1ca307b5..63793bab03a 100644
--- a/app/views/users/phones/edit.html.slim
+++ b/app/views/users/phones/edit.html.slim
@@ -20,7 +20,7 @@ h1.h3.my0 = t('headings.edit_info.phone')
- if MfaPolicy.new(current_user).multiple_factors_enabled? && @user_phone_form.phone.present?
br
.sm-col-8.mb3
- = button_to t('forms.phone.buttons.delete'), manage_phone_url, \
+ = button_to t('forms.phone.buttons.delete'), manage_phone_url(id: params[:id]), \
class: 'btn btn-danger btn-wide', \
method: :delete
= render 'shared/cancel', link: account_path
diff --git a/config/locales/headings/en.yml b/config/locales/headings/en.yml
index bb563307eaa..9f733dfd293 100644
--- a/config/locales/headings/en.yml
+++ b/config/locales/headings/en.yml
@@ -12,6 +12,8 @@ en:
verified_account: Verified Account
account_recovery_setup:
piv_cac_linked: Your PIV/CAC card is linked to your account
+ add_info:
+ phone: Add a phone number
cancellations:
confirmation: You have cancelled verifying your identity with login.gov
prompt: Are you sure you want to cancel?
diff --git a/config/locales/headings/es.yml b/config/locales/headings/es.yml
index 58bfd4defc4..92a0f466cfd 100644
--- a/config/locales/headings/es.yml
+++ b/config/locales/headings/es.yml
@@ -12,6 +12,8 @@ es:
verified_account: Cuenta verificada
account_recovery_setup:
piv_cac_linked: Tu tarjeta PIV/CAC está vinculada a tu cuenta
+ add_info:
+ phone: Agregar un número de teléfono
cancellations:
confirmation: Ha cancelado la verificación de su identidad con login.gov
prompt: "¿Estas seguro que quieres cancelar?"
diff --git a/config/locales/headings/fr.yml b/config/locales/headings/fr.yml
index 9377c7c3812..ad9ae7794ce 100644
--- a/config/locales/headings/fr.yml
+++ b/config/locales/headings/fr.yml
@@ -12,6 +12,8 @@ fr:
verified_account: Compte vérifié
account_recovery_setup:
piv_cac_linked: Votre carte PIV/CAC est liée à votre compte
+ add_info:
+ phone: Ajouter un numéro de téléphone
cancellations:
confirmation: Vous avez annulé la vérification de votre identité avec login.gov
prompt: Es-tu sûre de vouloir annuler?
diff --git a/config/locales/titles/en.yml b/config/locales/titles/en.yml
index 6e5379493ed..6da95bf39e4 100644
--- a/config/locales/titles/en.yml
+++ b/config/locales/titles/en.yml
@@ -4,6 +4,8 @@ en:
account: Account
account_locked: Account temporarily locked
account_recovery_setup: Account Recovery Setup
+ add_info:
+ phone: Add a phone number
confirmations:
new: Resend confirmation instructions for your account
show: Choose a password
diff --git a/config/locales/titles/es.yml b/config/locales/titles/es.yml
index 470668e9336..fec5104b40c 100644
--- a/config/locales/titles/es.yml
+++ b/config/locales/titles/es.yml
@@ -4,6 +4,8 @@ es:
account: Cuenta
account_locked: Cuenta bloqueada temporalmente
account_recovery_setup: Ajustes de recuperación de cuenta
+ add_info:
+ phone: Agregar un número de teléfono
confirmations:
new: Reenviar instrucciones de confirmación de su cuenta
show: Elija una contraseña
diff --git a/config/locales/titles/fr.yml b/config/locales/titles/fr.yml
index 53c783c4a37..c05dfdf1fdd 100644
--- a/config/locales/titles/fr.yml
+++ b/config/locales/titles/fr.yml
@@ -4,6 +4,8 @@ fr:
account: Compte
account_locked: Compte temporairement verrouillé
account_recovery_setup: Configuration de la récupération du compte
+ add_info:
+ phone: Ajouter un numéro de téléphone
confirmations:
new: Envoyer les instructions de confirmation pour votre compte
show: Choisissez un mot de passe
diff --git a/config/routes.rb b/config/routes.rb
index fac1e4c448b..d08c590d510 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -137,6 +137,8 @@
match '/manage/email' => 'users/emails#update', via: %i[patch put]
get '/manage/password' => 'users/passwords#edit'
patch '/manage/password' => 'users/passwords#update'
+ get '/add/phone' => 'users/phones#add'
+ post '/add/phone' => 'users/phones#create'
get '/manage/phone' => 'users/phones#edit'
match '/manage/phone' => 'users/phones#update', via: %i[patch put]
delete '/manage/phone' => 'users/phones#delete'
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 8f745eba529..2ea0bf80d33 100644
--- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
@@ -281,6 +281,8 @@
context 'user has an existing phone number' do
context 'user enters a valid code' do
before do
+ controller.user_session[:phone_id] = \
+ MfaContext.new(subject.current_user).phone_configurations.last.id
post(
:create,
params: {
diff --git a/spec/controllers/users/phones_controller_spec.rb b/spec/controllers/users/phones_controller_spec.rb
index 4f2cbb5b602..3ddb880700a 100644
--- a/spec/controllers/users/phones_controller_spec.rb
+++ b/spec/controllers/users/phones_controller_spec.rb
@@ -216,4 +216,35 @@
end
end
end
+
+ context 'user adds phone' do
+ let(:user) { create(:user, :signed_up, with: { phone: '+1 (202) 555-1234' }) }
+ let(:new_phone) { '202-555-4321' }
+ before do
+ stub_sign_in(user)
+
+ stub_analytics
+ allow(@analytics).to receive(:track_event)
+ end
+
+ it 'gives the user a form to enter a new phone number' do
+ get :add
+ expect(response).to render_template(:add)
+ end
+
+ it 'lets user know they need to confirm their new phone' do
+ put :create, params: {
+ user_phone_form: { phone: new_phone,
+ international_code: 'US',
+ otp_delivery_preference: 'sms' },
+ }
+ expect(flash[:notice]).to eq t('devise.registrations.phone_update_needs_confirmation')
+ expect(
+ MfaContext.new(user).phone_configurations.reload.first.phone
+ ).to_not eq '+1 202-555-4321'
+ expect(response).to redirect_to(otp_send_path(otp_delivery_selection_form:
+ { otp_delivery_preference: 'sms' }))
+ expect(subject.user_session[:context]).to eq 'confirmation'
+ end
+ end
end
diff --git a/spec/features/two_factor_authentication/change_factor_spec.rb b/spec/features/two_factor_authentication/change_factor_spec.rb
index b9d03bf7324..311f5fdfa3d 100644
--- a/spec/features/two_factor_authentication/change_factor_spec.rb
+++ b/spec/features/two_factor_authentication/change_factor_spec.rb
@@ -26,7 +26,7 @@
MfaContext.new(user).phone_configurations.reload.first.confirmed_at
new_phone = '+1 703-555-0100'
- visit manage_phone_path
+ visit manage_phone_path(id: user.phone_configurations.first.id)
expect(page).to have_content t('help_text.change_factor', factor: 'phone')
diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb
index 5f36a1173a9..31a6dd3a34e 100644
--- a/spec/features/users/sign_in_spec.rb
+++ b/spec/features/users/sign_in_spec.rb
@@ -396,7 +396,7 @@
expect(VoiceOtpSenderJob).to_not have_received(:perform_later)
expect(SmsOtpSenderJob).to have_received(:perform_later).exactly(:once)
expect(page).
- to have_current_path(login_two_factor_path(otp_delivery_preference: 'sms'))
+ to have_current_path(login_two_factor_path(otp_delivery_preference: 'sms', reauthn: false))
expect(page).to have_content t(
'two_factor_authentication.otp_delivery_preference.phone_unsupported',
location: 'India'
diff --git a/spec/services/otp_delivery_preference_updater_spec.rb b/spec/services/otp_delivery_preference_updater_spec.rb
index aa48a0e1e1b..e21f9bb44ec 100644
--- a/spec/services/otp_delivery_preference_updater_spec.rb
+++ b/spec/services/otp_delivery_preference_updater_spec.rb
@@ -5,7 +5,7 @@
OtpDeliveryPreferenceUpdater.new(
user: build_stubbed(:user, otp_delivery_preference: 'sms'),
preference: 'sms',
- context: 'authentication'
+ phone_id: 1
)
end
@@ -25,7 +25,7 @@
updater = OtpDeliveryPreferenceUpdater.new(
user: user,
preference: 'sms',
- context: 'authentication'
+ phone_id: 1
)
attributes = { otp_delivery_preference: 'sms' }
@@ -45,7 +45,7 @@
updater = OtpDeliveryPreferenceUpdater.new(
user: nil,
preference: 'sms',
- context: 'idv'
+ phone_id: 1
)
expect(UpdateUser).to_not receive(:new)
diff --git a/spec/services/update_user_spec.rb b/spec/services/update_user_spec.rb
index cd55785b766..59776123cdf 100644
--- a/spec/services/update_user_spec.rb
+++ b/spec/services/update_user_spec.rb
@@ -16,21 +16,6 @@
context 'with a phone already configured' do
let(:user) { create(:user, :with_phone) }
- it 'updates the phone configuration' do
- confirmed_at = 1.day.ago.change(usec: 0)
- attributes = {
- otp_delivery_preference: 'voice',
- phone: '+1 222 333-4444',
- phone_confirmed_at: confirmed_at,
- }
- updater = UpdateUser.new(user: user, attributes: attributes)
- updater.call
- phone_configuration = user.phone_configurations.reload.first
- expect(phone_configuration.delivery_preference).to eq 'voice'
- expect(phone_configuration.confirmed_at).to eq confirmed_at
- expect(phone_configuration.phone).to eq '+1 222 333-4444'
- end
-
it 'does not delete the phone configuration' do
attributes = { phone: nil }
updater = UpdateUser.new(user: user, attributes: attributes)
diff --git a/spec/views/accounts/show.html.slim_spec.rb b/spec/views/accounts/show.html.slim_spec.rb
index 8d598c732df..be4c5c10cde 100644
--- a/spec/views/accounts/show.html.slim_spec.rb
+++ b/spec/views/accounts/show.html.slim_spec.rb
@@ -166,18 +166,18 @@
render
expect(rendered).to have_link(
- t('account.index.phone_add'), href: manage_phone_path
+ t('account.index.phone_add'), href: add_phone_path
)
end
end
context 'user has a phone' do
- it 'shows no add phone link' do
+ it 'shows add phone link' do
render
- expect(rendered).to_not have_content t('account.index.phone_add')
- expect(rendered).to_not have_link(
- t('account.index.phone_add'), href: manage_phone_path
+ expect(rendered).to have_content t('account.index.phone_add')
+ expect(rendered).to have_link(
+ t('account.index.phone_add'), href: add_phone_path
)
end
@@ -185,7 +185,7 @@
render
expect(rendered).to have_link(
- t('account.index.phone'), href: manage_phone_url
+ t('account.index.phone'), href: manage_phone_url(id: user.phone_configurations.first.id)
)
end
end