mirror of
https://github.com/go-gitea/gitea
synced 2025-01-05 07:24:25 +00:00
Support for email addresses containing uppercase characters when activating user account (#32998)
Fix #32807 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
3c00e89129
commit
2d1a171dc7
@ -357,8 +357,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres
|
|||||||
if user := GetVerifyUser(ctx, code); user != nil {
|
if user := GetVerifyUser(ctx, code); user != nil {
|
||||||
// time limit code
|
// time limit code
|
||||||
prefix := code[:base.TimeLimitCodeLength]
|
prefix := code[:base.TimeLimitCodeLength]
|
||||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
|
opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email}
|
||||||
|
data := makeTimeLimitCodeHashData(opts, user)
|
||||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||||
emailAddress := &EmailAddress{UID: user.ID, Email: email}
|
emailAddress := &EmailAddress{UID: user.ID, Email: email}
|
||||||
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
|
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
|
||||||
@ -486,10 +486,10 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
|
|||||||
|
|
||||||
// Activate/deactivate a user's primary email address and account
|
// Activate/deactivate a user's primary email address and account
|
||||||
if addr.IsPrimary {
|
if addr.IsPrimary {
|
||||||
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email})
|
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !exist {
|
} else if !exist || !strings.EqualFold(user.Email, email) {
|
||||||
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
|
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +181,8 @@ func (u *User) BeforeUpdate() {
|
|||||||
u.MaxRepoCreation = -1
|
u.MaxRepoCreation = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Organization does not need email
|
// FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field
|
||||||
|
// This trick could be removed in new releases to display the user inputed email as-is.
|
||||||
u.Email = strings.ToLower(u.Email)
|
u.Email = strings.ToLower(u.Email)
|
||||||
if !u.IsOrganization() {
|
if !u.IsOrganization() {
|
||||||
if len(u.AvatarEmail) == 0 {
|
if len(u.AvatarEmail) == 0 {
|
||||||
@ -310,17 +311,6 @@ func (u *User) OrganisationLink() string {
|
|||||||
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateEmailActivateCode generates an activate code based on user information and given e-mail.
|
|
||||||
func (u *User) GenerateEmailActivateCode(email string) string {
|
|
||||||
code := base.CreateTimeLimitCode(
|
|
||||||
fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands),
|
|
||||||
setting.Service.ActiveCodeLives, time.Now(), nil)
|
|
||||||
|
|
||||||
// Add tail hex username
|
|
||||||
code += hex.EncodeToString([]byte(u.LowerName))
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserFollowers returns range of user's followers.
|
// GetUserFollowers returns range of user's followers.
|
||||||
func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {
|
func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {
|
||||||
sess := db.GetEngine(ctx).
|
sess := db.GetEngine(ctx).
|
||||||
@ -863,12 +853,38 @@ func GetVerifyUser(ctx context.Context, code string) (user *User) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyUserActiveCode verifies active code when active account
|
type TimeLimitCodePurpose string
|
||||||
func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
|
|
||||||
|
const (
|
||||||
|
TimeLimitCodeActivateAccount TimeLimitCodePurpose = "activate_account"
|
||||||
|
TimeLimitCodeActivateEmail TimeLimitCodePurpose = "activate_email"
|
||||||
|
TimeLimitCodeResetPassword TimeLimitCodePurpose = "reset_password"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimeLimitCodeOptions struct {
|
||||||
|
Purpose TimeLimitCodePurpose
|
||||||
|
NewEmail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTimeLimitCodeHashData(opts *TimeLimitCodeOptions, u *User) string {
|
||||||
|
return fmt.Sprintf("%s|%d|%s|%s|%s|%s", opts.Purpose, u.ID, strings.ToLower(util.IfZero(opts.NewEmail, u.Email)), u.LowerName, u.Passwd, u.Rands)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateUserTimeLimitCode generates a time-limit code based on user information and given e-mail.
|
||||||
|
// TODO: need to use cache or db to store it to make sure a code can only be consumed once
|
||||||
|
func GenerateUserTimeLimitCode(opts *TimeLimitCodeOptions, u *User) string {
|
||||||
|
data := makeTimeLimitCodeHashData(opts, u)
|
||||||
|
code := base.CreateTimeLimitCode(data, setting.Service.ActiveCodeLives, time.Now(), nil)
|
||||||
|
code += hex.EncodeToString([]byte(u.LowerName)) // Add tail hex username
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyUserTimeLimitCode verifies the time-limit code
|
||||||
|
func VerifyUserTimeLimitCode(ctx context.Context, opts *TimeLimitCodeOptions, code string) (user *User) {
|
||||||
if user = GetVerifyUser(ctx, code); user != nil {
|
if user = GetVerifyUser(ctx, code); user != nil {
|
||||||
// time limit code
|
// time limit code
|
||||||
prefix := code[:base.TimeLimitCodeLength]
|
prefix := code[:base.TimeLimitCodeLength]
|
||||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands)
|
data := makeTimeLimitCodeHashData(opts, user)
|
||||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
@ -689,7 +689,7 @@ func Activate(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
||||||
user := user_model.VerifyUserActiveCode(ctx, code)
|
user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code)
|
||||||
if user == nil { // if code is wrong
|
if user == nil { // if code is wrong
|
||||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
||||||
return
|
return
|
||||||
@ -734,7 +734,7 @@ func ActivatePost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
||||||
user := user_model.VerifyUserActiveCode(ctx, code)
|
user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code)
|
||||||
if user == nil { // if code is wrong
|
if user == nil { // if code is wrong
|
||||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
||||||
return
|
return
|
||||||
|
@ -113,7 +113,7 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fail early, don't frustrate the user
|
// Fail early, don't frustrate the user
|
||||||
u := user_model.VerifyUserActiveCode(ctx, code)
|
u := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}, code)
|
||||||
if u == nil {
|
if u == nil {
|
||||||
ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
|
ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -93,7 +93,8 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
|
|||||||
// No mail service configured
|
// No mail service configured
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
|
opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}
|
||||||
|
sendUserMail(locale.Language(), u, mailAuthActivate, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.activate_account"), "activate account")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendResetPasswordMail sends a password reset mail to the user
|
// SendResetPasswordMail sends a password reset mail to the user
|
||||||
@ -103,7 +104,8 @@ func SendResetPasswordMail(u *user_model.User) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
locale := translation.NewLocale(u.Language)
|
locale := translation.NewLocale(u.Language)
|
||||||
sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account")
|
opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}
|
||||||
|
sendUserMail(u.Language, u, mailAuthResetPassword, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.reset_password"), "recover account")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendActivateEmailMail sends confirmation email to confirm new email address
|
// SendActivateEmailMail sends confirmation email to confirm new email address
|
||||||
@ -113,11 +115,12 @@ func SendActivateEmailMail(u *user_model.User, email string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
locale := translation.NewLocale(u.Language)
|
locale := translation.NewLocale(u.Language)
|
||||||
|
opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateEmail, NewEmail: email}
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"locale": locale,
|
"locale": locale,
|
||||||
"DisplayName": u.DisplayName(),
|
"DisplayName": u.DisplayName(),
|
||||||
"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
|
"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
|
||||||
"Code": u.GenerateEmailActivateCode(email),
|
"Code": user_model.GenerateUserTimeLimitCode(opts, u),
|
||||||
"Email": email,
|
"Email": email,
|
||||||
"Language": locale.Language(),
|
"Language": locale.Language(),
|
||||||
}
|
}
|
||||||
|
@ -274,7 +274,8 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) {
|
|||||||
user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist")
|
user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com"))
|
activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user)
|
||||||
|
activateURL := fmt.Sprintf("/user/activate?code=%s", activationCode)
|
||||||
req = NewRequestWithValues(t, "POST", activateURL, map[string]string{
|
req = NewRequestWithValues(t, "POST", activateURL, map[string]string{
|
||||||
"password": "examplePassword!1",
|
"password": "examplePassword!1",
|
||||||
})
|
})
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
@ -99,34 +100,39 @@ func TestSignupEmailActive(t *testing.T) {
|
|||||||
|
|
||||||
// try to sign up and send the activation email
|
// try to sign up and send the activation email
|
||||||
req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
|
req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
|
||||||
"user_name": "test-user-1",
|
"user_name": "Test-User-1",
|
||||||
"email": "email-1@example.com",
|
"email": "EmAiL-1@example.com",
|
||||||
"password": "password1",
|
"password": "password1",
|
||||||
"retype": "password1",
|
"retype": "password1",
|
||||||
})
|
})
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>email-1@example.com</b>.`)
|
assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>EmAiL-1@example.com</b>.`)
|
||||||
|
|
||||||
// access "user/activate" means trying to re-send the activation email
|
// access "user/activate" means trying to re-send the activation email
|
||||||
session := loginUserWithPassword(t, "test-user-1", "password1")
|
session := loginUserWithPassword(t, "test-user-1", "password1")
|
||||||
resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK)
|
resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK)
|
||||||
assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently")
|
assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently")
|
||||||
|
|
||||||
// access anywhere else will see a "Activate Your Account" prompt, and there is a chance to change email
|
// access anywhere else will see an "Activate Your Account" prompt, and there is a chance to change email
|
||||||
resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/issues"), http.StatusOK)
|
resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/issues"), http.StatusOK)
|
||||||
assert.Contains(t, resp.Body.String(), `<input id="change-email" name="change_email" `)
|
assert.Contains(t, resp.Body.String(), `<input id="change-email" name="change_email" `)
|
||||||
|
|
||||||
// post to "user/activate" with a new email
|
// post to "user/activate" with a new email
|
||||||
session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/activate", map[string]string{"change_email": "email-changed@example.com"}), http.StatusSeeOther)
|
session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/activate", map[string]string{"change_email": "email-changed@example.com"}), http.StatusSeeOther)
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
|
||||||
assert.Equal(t, "email-changed@example.com", user.Email)
|
assert.Equal(t, "email-changed@example.com", user.Email)
|
||||||
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "email-changed@example.com"})
|
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "email-changed@example.com"})
|
||||||
assert.False(t, email.IsActivated)
|
assert.False(t, email.IsActivated)
|
||||||
assert.True(t, email.IsPrimary)
|
assert.True(t, email.IsPrimary)
|
||||||
|
|
||||||
|
// generate an activation code from lower-cased email
|
||||||
|
activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user)
|
||||||
|
// and update the user email to case-sensitive, it shouldn't affect the verification later
|
||||||
|
_, _ = db.Exec(db.DefaultContext, "UPDATE `user` SET email=? WHERE id=?", "EmAiL-changed@example.com", user.ID)
|
||||||
|
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
|
||||||
|
assert.Equal(t, "EmAiL-changed@example.com", user.Email)
|
||||||
|
|
||||||
// access "user/activate" with a valid activation code, then get the "verify password" page
|
// access "user/activate" with a valid activation code, then get the "verify password" page
|
||||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
|
|
||||||
activationCode := user.GenerateEmailActivateCode(user.Email)
|
|
||||||
resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate?code="+activationCode), http.StatusOK)
|
resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate?code="+activationCode), http.StatusOK)
|
||||||
assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
|
assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
|
||||||
|
|
||||||
@ -138,7 +144,7 @@ func TestSignupEmailActive(t *testing.T) {
|
|||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
assert.Contains(t, resp.Body.String(), `Your password does not match`)
|
assert.Contains(t, resp.Body.String(), `Your password does not match`)
|
||||||
assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
|
assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
|
||||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
|
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
|
||||||
assert.False(t, user.IsActive)
|
assert.False(t, user.IsActive)
|
||||||
|
|
||||||
// then use a correct password, the user should be activated
|
// then use a correct password, the user should be activated
|
||||||
@ -148,6 +154,6 @@ func TestSignupEmailActive(t *testing.T) {
|
|||||||
})
|
})
|
||||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
assert.Equal(t, "/", test.RedirectURL(resp))
|
assert.Equal(t, "/", test.RedirectURL(resp))
|
||||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
|
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
|
||||||
assert.True(t, user.IsActive)
|
assert.True(t, user.IsActive)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user