Commit 4a3f6041 authored by Drew Blessing's avatar Drew Blessing Committed by Drew Blessing

Unknown sign-in email styling

Add styling to the unknown sign-in email so it looks more
legitimate and contains more useful information about what
happened.
parent 0a066d83
...@@ -26,6 +26,6 @@ module KnownSignIn ...@@ -26,6 +26,6 @@ module KnownSignIn
end end
def notify_user def notify_user
current_user.notification_service.unknown_sign_in(current_user, request.remote_ip) current_user.notification_service.unknown_sign_in(current_user, request.remote_ip, current_user.current_sign_in_at)
end end
end end
...@@ -45,13 +45,20 @@ module Emails ...@@ -45,13 +45,20 @@ module Emails
end end
end end
def unknown_sign_in_email(user, ip) def unknown_sign_in_email(user, ip, time)
@user = user @user = user
@ip = ip @ip = ip
@time = time
@target_url = edit_profile_password_url @target_url = edit_profile_password_url
Gitlab::I18n.with_locale(@user.preferred_language) do Gitlab::I18n.with_locale(@user.preferred_language) do
mail(to: @user.notification_email, subject: subject(_("Unknown sign-in from new location"))) mail(
to: @user.notification_email,
subject: subject(_("%{host} sign-in from new location") % { host: Gitlab.config.gitlab.host })
) do |format|
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
end
end end
end end
end end
......
...@@ -162,7 +162,7 @@ class NotifyPreview < ActionMailer::Preview ...@@ -162,7 +162,7 @@ class NotifyPreview < ActionMailer::Preview
end end
def unknown_sign_in_email def unknown_sign_in_email
Notify.unknown_sign_in_email(user, '127.0.0.1').message Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message
end end
private private
......
...@@ -36,7 +36,7 @@ class ActiveSession ...@@ -36,7 +36,7 @@ class ActiveSession
timestamp = Time.current timestamp = Time.current
active_user_session = new( active_user_session = new(
ip_address: request.ip, ip_address: request.remote_ip,
browser: client.name, browser: client.name,
os: client.os_name, os: client.os_name,
device_name: client.device_name, device_name: client.device_name,
......
...@@ -68,10 +68,10 @@ class NotificationService ...@@ -68,10 +68,10 @@ class NotificationService
# Notify a user when a previously unknown IP or device is used to # Notify a user when a previously unknown IP or device is used to
# sign in to their account # sign in to their account
def unknown_sign_in(user, ip) def unknown_sign_in(user, ip, time)
return unless user.can?(:receive_notifications) return unless user.can?(:receive_notifications)
mailer.unknown_sign_in_email(user, ip).deliver_later mailer.unknown_sign_in_email(user, ip, time).deliver_later
end end
# When create an issue we should send an email to: # When create an issue we should send an email to:
......
%p - default_font = "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"
= _('Hi %{username}!') % { username: sanitize_name(@user.name) } - default_style = "#{default_font}font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"
%p - spacer_style = "#{default_font};height:18px;font-size:18px;line-height:18px;"
= _('A sign-in to your account has been made from the following IP address: %{ip}.') % { ip: @ip }
%p %tr.alert
%td{ style: "#{default_font}padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#FC6D26;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
%td{ style: "#{default_font}vertical-align:middle;color:#ffffff;text-align:center;" }
%span
= _("Your %{host} account was signed in to from a new location") % { host: Gitlab.config.gitlab.host }
%tr.spacer
%td{ style: spacer_style }
&nbsp;
%tr.section
%td{ style: "#{default_font};padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
%td{ style: default_style }
= _('Hostname')
%td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;" }
= Gitlab.config.gitlab.host
%tr
%td{ style: "#{default_style}border-top:1px solid #ededed;" }
= _('IP Address')
%td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%span.muted{ style: "color:#333333;text-decoration:none;" }
= @ip
%tr
%td{ style: "#{default_style}border-top:1px solid #ededed;" }
= _('Time')
%td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
= @time.strftime('%Y-%m-%d %l:%M:%S %p %Z')
%tr.spacer
%td{ style: spacer_style }
&nbsp;
%tr.section
%td{ style: "#{default_font};line-height:1.4;text-align:center;padding:0 15px;overflow:hidden;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" }
%tbody
%tr{ style: 'width:100%;' }
%td{ style: "#{default_style}text-align:center;" }
- password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' } - password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' }
= _('If you recently signed in and recognize the IP address, you may disregard this email.') = _('If you recently signed in and recognize the IP address, you may disregard this email.')
%p
= _('If you did not recently sign in, you should immediately %{password_link_start}change your password%{password_link_end}.').html_safe % { password_link_start: password_link_start, password_link_end: '</a>'.html_safe } = _('If you did not recently sign in, you should immediately %{password_link_start}change your password%{password_link_end}.').html_safe % { password_link_start: password_link_start, password_link_end: '</a>'.html_safe }
= _('Passwords should be unique and not used for any other sites or services.') = _('Passwords should be unique and not used for any other sites or services.')
- unless @user.two_factor_enabled? - unless @user.two_factor_enabled?
%p %p
- mfa_link_start = '<a href="https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html" target="_blank">'.html_safe - mfa_link_start = '<a href="https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html" target="_blank">'.html_safe
= _('To further protect your account, consider configuring a %{mfa_link_start}two-factor authentication%{mfa_link_end} method.').html_safe % { mfa_link_start: mfa_link_start, mfa_link_end: '</a>'.html_safe } = _('To further protect your account, consider configuring a %{mfa_link_start}two-factor authentication%{mfa_link_end} method.').html_safe % { mfa_link_start: mfa_link_start, mfa_link_end: '</a>'.html_safe }
---
title: Improve new/unknown sign-in email styling
merge_request: 32808
author:
type: changed
# Email notification for unknown sign-ins # Email notification for unknown sign-ins
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27211) in GitLab 13.0.
When a user successfully signs in from a previously unknown IP address, When a user successfully signs in from a previously unknown IP address,
GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially
malicious or unauthorized sign-ins. malicious or unauthorized sign-ins.
...@@ -13,4 +15,4 @@ There are two methods used to identify a known sign-in: ...@@ -13,4 +15,4 @@ There are two methods used to identify a known sign-in:
## Example email ## Example email
![Unknown sign in email](./img/unknown_sign_in_email_v13_0.png) ![Unknown sign in email](./img/unknown_sign_in_email_v13_1.png)
...@@ -343,6 +343,9 @@ msgstr "" ...@@ -343,6 +343,9 @@ msgstr ""
msgid "%{group_name} uses group managed accounts. You need to create a new GitLab account which will be managed by %{group_name}." msgid "%{group_name} uses group managed accounts. You need to create a new GitLab account which will be managed by %{group_name}."
msgstr "" msgstr ""
msgid "%{host} sign-in from new location"
msgstr ""
msgid "%{icon}You are about to add %{usersTag} people to the discussion. Proceed with caution." msgid "%{icon}You are about to add %{usersTag} people to the discussion. Proceed with caution."
msgstr "" msgstr ""
...@@ -950,9 +953,6 @@ msgstr "" ...@@ -950,9 +953,6 @@ msgstr ""
msgid "A sign-in to your account has been made from the following IP address: %{ip}" msgid "A sign-in to your account has been made from the following IP address: %{ip}"
msgstr "" msgstr ""
msgid "A sign-in to your account has been made from the following IP address: %{ip}."
msgstr ""
msgid "A subscription will trigger a new pipeline on the default branch of this project when a pipeline successfully completes for a new tag on the %{default_branch_docs} of the subscribed project." msgid "A subscription will trigger a new pipeline on the default branch of this project when a pipeline successfully completes for a new tag on the %{default_branch_docs} of the subscribed project."
msgstr "" msgstr ""
...@@ -11290,6 +11290,9 @@ msgstr "" ...@@ -11290,6 +11290,9 @@ msgstr ""
msgid "Hook was successfully updated." msgid "Hook was successfully updated."
msgstr "" msgstr ""
msgid "Hostname"
msgstr ""
msgid "Hour (UTC)" msgid "Hour (UTC)"
msgstr "" msgstr ""
...@@ -23248,9 +23251,6 @@ msgstr "" ...@@ -23248,9 +23251,6 @@ msgstr ""
msgid "Unknown response text" msgid "Unknown response text"
msgstr "" msgstr ""
msgid "Unknown sign-in from new location"
msgstr ""
msgid "Unlimited" msgid "Unlimited"
msgstr "" msgstr ""
...@@ -25216,6 +25216,9 @@ msgstr "" ...@@ -25216,6 +25216,9 @@ msgstr ""
msgid "YouTube" msgid "YouTube"
msgstr "" msgstr ""
msgid "Your %{host} account was signed in to from a new location"
msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}." msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}."
msgstr "" msgstr ""
......
...@@ -160,38 +160,48 @@ describe Emails::Profile do ...@@ -160,38 +160,48 @@ describe Emails::Profile do
describe 'user unknown sign in email' do describe 'user unknown sign in email' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:ip) { '169.0.0.1' } let_it_be(:ip) { '169.0.0.1' }
let_it_be(:current_time) { Time.current }
let_it_be(:email) { Notify.unknown_sign_in_email(user, ip, current_time) }
subject { Notify.unknown_sign_in_email(user, ip) } subject { email }
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'a user cannot unsubscribe through footer link'
it 'is sent to the user' do it 'is sent to the user' do
expect(subject).to deliver_to user.email is_expected.to deliver_to user.email
end end
it 'has the correct subject' do it 'has the correct subject' do
expect(subject).to have_subject /^Unknown sign-in from new location$/ is_expected.to have_subject "#{Gitlab.config.gitlab.host} sign-in from new location"
end
it 'mentions the new sign-in IP' do
is_expected.to have_body_text ip
end end
it 'mentions the unknown sign-in IP' do it 'mentioned the time' do
expect(subject).to have_body_text /A sign-in to your account has been made from the following IP address: #{ip}./ is_expected.to have_body_text current_time.strftime('%Y-%m-%d %l:%M:%S %p %Z')
end end
it 'includes a link to the change password page' do it 'includes a link to the change password documentation' do
expect(subject).to have_body_text /#{edit_profile_password_path}/ is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/#changing-your-password'
end end
it 'mentions two factor authentication when two factor is not enabled' do it 'mentions two factor authentication when two factor is not enabled' do
expect(subject).to have_body_text /two-factor authentication/ is_expected.to have_body_text 'two-factor authentication'
end
it 'includes a link to two-factor authentication documentation' do
is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html'
end end
context 'when two factor authentication is enabled' do context 'when two factor authentication is enabled' do
it 'does not mention two factor authentication' do let(:user) { create(:user, :two_factor) }
two_factor_user = create(:user, :two_factor)
expect( Notify.unknown_sign_in_email(two_factor_user, ip) ) it 'does not mention two factor authentication' do
expect( Notify.unknown_sign_in_email(user, ip, current_time) )
.not_to have_body_text /two-factor authentication/ .not_to have_body_text /two-factor authentication/
end end
end end
......
...@@ -16,7 +16,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do ...@@ -16,7 +16,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
double(:request, { double(:request, {
user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 ' \ user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 ' \
'(KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]', '(KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]',
ip: '127.0.0.1', remote_ip: '127.0.0.1',
session: session session: session
}) })
end end
......
...@@ -243,11 +243,12 @@ describe NotificationService, :mailer do ...@@ -243,11 +243,12 @@ describe NotificationService, :mailer do
describe '#unknown_sign_in' do describe '#unknown_sign_in' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:ip) { '127.0.0.1' } let_it_be(:ip) { '127.0.0.1' }
let_it_be(:time) { Time.current }
subject { notification.unknown_sign_in(user, ip) } subject { notification.unknown_sign_in(user, ip, time) }
it 'sends email to the user' do it 'sends email to the user' do
expect { subject }.to have_enqueued_email(user, ip, mail: 'unknown_sign_in_email') expect { subject }.to have_enqueued_email(user, ip, time, mail: 'unknown_sign_in_email')
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment