[SECURITY] Notify users about account security changes

- Currently if the password, primary mail, TOTP or security keys are
changed, no notification is made of that and makes compromising an
account a bit easier as it's essentially undetectable until the original
person tries to log in. Although other changes should be made as
well (re-authing before allowing a password change), this should go a
long way of improving the account security in Forgejo.
- Adds a mail notification for password and primary mail changes. For
the primary mail change, a mail notification is sent to the old primary
mail.
- Add a mail notification when TOTP or a security keys is removed, if no
other 2FA method is configured the mail will also contain that 2FA is
no longer needed to log into their account.
- `MakeEmailAddressPrimary` is refactored to the user service package,
as it now involves calling the mailer service.
- Unit tests added.
- Integration tests added.
This commit is contained in:
Gusted 2024-07-23 00:17:06 +02:00
parent ded237ee77
commit 4383da91bd
No known key found for this signature in database
GPG key ID: FD821B732837125F
24 changed files with 543 additions and 116 deletions

View file

@ -17,6 +17,7 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
auth_model "code.gitea.io/gitea/models/auth"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@ -35,10 +36,14 @@ import (
)
const (
mailAuthActivate base.TplName = "auth/activate"
mailAuthActivateEmail base.TplName = "auth/activate_email"
mailAuthResetPassword base.TplName = "auth/reset_passwd"
mailAuthRegisterNotify base.TplName = "auth/register_notify"
mailAuthActivate base.TplName = "auth/activate"
mailAuthActivateEmail base.TplName = "auth/activate_email"
mailAuthResetPassword base.TplName = "auth/reset_passwd"
mailAuthRegisterNotify base.TplName = "auth/register_notify"
mailAuthPasswordChange base.TplName = "auth/password_change"
mailAuthPrimaryMailChange base.TplName = "auth/primary_mail_change"
mailAuth2faDisabled base.TplName = "auth/2fa_disabled"
mailAuthRemovedSecurityKey base.TplName = "auth/removed_security_key"
mailNotifyCollaborator base.TplName = "notify/collaborator"
@ -561,3 +566,133 @@ func fromDisplayName(u *user_model.User) string {
}
return u.GetCompleteName()
}
// SendPasswordChange informs the user on their primary email address that
// their password was changed.
func SendPasswordChange(u *user_model.User) error {
if setting.MailService == nil {
return nil
}
locale := translation.NewLocale(u.Language)
data := map[string]any{
"locale": locale,
"DisplayName": u.DisplayName(),
"Username": u.Name,
"Language": locale.Language(),
}
var content bytes.Buffer
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthPasswordChange), data); err != nil {
return err
}
msg := NewMessage(u.EmailTo(), locale.TrString("mail.password_change.subject"), content.String())
msg.Info = fmt.Sprintf("UID: %d, password change notification", u.ID)
SendAsync(msg)
return nil
}
// SendPrimaryMailChange informs the user on their old primary email address
// that it's no longer used as primary mail and will no longer receive
// notification on that email address.
func SendPrimaryMailChange(u *user_model.User, oldPrimaryEmail string) error {
if setting.MailService == nil {
return nil
}
locale := translation.NewLocale(u.Language)
data := map[string]any{
"locale": locale,
"NewPrimaryMail": u.Email,
"DisplayName": u.DisplayName(),
"Username": u.Name,
"Language": locale.Language(),
}
var content bytes.Buffer
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthPrimaryMailChange), data); err != nil {
return err
}
msg := NewMessage(u.EmailTo(oldPrimaryEmail), locale.TrString("mail.primary_mail_change.subject"), content.String())
msg.Info = fmt.Sprintf("UID: %d, primary email change notification", u.ID)
SendAsync(msg)
return nil
}
// SendDisabledTOTP informs the user that their totp has been disabled.
func SendDisabledTOTP(ctx context.Context, u *user_model.User) error {
if setting.MailService == nil {
return nil
}
locale := translation.NewLocale(u.Language)
hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(ctx, u.ID)
if err != nil {
return err
}
data := map[string]any{
"locale": locale,
"HasWebAuthn": hasWebAuthn,
"DisplayName": u.DisplayName(),
"Username": u.Name,
"Language": locale.Language(),
}
var content bytes.Buffer
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuth2faDisabled), data); err != nil {
return err
}
msg := NewMessage(u.EmailTo(), locale.TrString("mail.totp_disabled.subject"), content.String())
msg.Info = fmt.Sprintf("UID: %d, 2fa disabled notification", u.ID)
SendAsync(msg)
return nil
}
// SendRemovedWebAuthn informs the user that one of their security keys has been removed.
func SendRemovedSecurityKey(ctx context.Context, u *user_model.User, securityKeyName string) error {
if setting.MailService == nil {
return nil
}
locale := translation.NewLocale(u.Language)
hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(ctx, u.ID)
if err != nil {
return err
}
hasTOTP, err := auth_model.HasTwoFactorByUID(ctx, u.ID)
if err != nil {
return err
}
data := map[string]any{
"locale": locale,
"HasWebAuthn": hasWebAuthn,
"HasTOTP": hasTOTP,
"SecurityKeyName": securityKeyName,
"DisplayName": u.DisplayName(),
"Username": u.Name,
"Language": locale.Language(),
}
var content bytes.Buffer
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthRemovedSecurityKey), data); err != nil {
return err
}
msg := NewMessage(u.EmailTo(), locale.TrString("mail.removed_security_key.subject"), content.String())
msg.Info = fmt.Sprintf("UID: %d, security key removed notification", u.ID)
SendAsync(msg)
return nil
}