mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-03 13:20:28 +00:00
[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:
parent
ded237ee77
commit
4383da91bd
24 changed files with 543 additions and 116 deletions
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue