mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-22 07:13:02 -05:00
fix: extend forgejo_auth_token
table
- Add a `purpose` column, this allows the `forgejo_auth_token` table to
be used by other parts of Forgejo, while still enjoying the
no-compromise architecture.
- Remove the 'roll your own crypto' time limited code functions and
migrate them to the `forgejo_auth_token` table. This migration ensures
generated codes can only be used for their purpose and ensure they are
invalidated after their usage by deleting it from the database, this
also should help making auditing of the security code easier, as we're
no longer trying to stuff a lot of data into a HMAC construction.
-Helper functions are rewritten to ensure a safe-by-design approach to
these tokens.
- Add the `forgejo_auth_token` to dbconsistency doctor and add it to the
`deleteUser` function.
- TODO: Add cron job to delete expired authorization tokens.
- Unit and integration tests added.
(cherry picked from commit 1ce33aa38d
)
v7: Removed migration - XORM can handle this case automatically without migration.
assert.Equal(t, `doesnotexist@example.com`, msgs[0].To) in tests
because v7 does not include the user name to the recipient.
This commit is contained in:
parent
5b53a150c0
commit
b770282d45
16 changed files with 429 additions and 240 deletions
|
@ -15,12 +15,31 @@ import (
|
|||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type AuthorizationPurpose string
|
||||
|
||||
var (
|
||||
// Used to store long term authorization tokens.
|
||||
LongTermAuthorization AuthorizationPurpose = "long_term_authorization"
|
||||
|
||||
// Used to activate a user account.
|
||||
UserActivation AuthorizationPurpose = "user_activation"
|
||||
|
||||
// Used to reset the password.
|
||||
PasswordReset AuthorizationPurpose = "password_reset"
|
||||
)
|
||||
|
||||
// Used to activate the specified email address for a user.
|
||||
func EmailActivation(email string) AuthorizationPurpose {
|
||||
return AuthorizationPurpose("email_activation:" + email)
|
||||
}
|
||||
|
||||
// AuthorizationToken represents a authorization token to a user.
|
||||
type AuthorizationToken struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UID int64 `xorm:"INDEX"`
|
||||
LookupKey string `xorm:"INDEX UNIQUE"`
|
||||
HashedValidator string
|
||||
Purpose AuthorizationPurpose `xorm:"NOT NULL DEFAULT 'long_term_authorization'"`
|
||||
Expiry timeutil.TimeStamp
|
||||
}
|
||||
|
||||
|
@ -41,7 +60,7 @@ func (authToken *AuthorizationToken) IsExpired() bool {
|
|||
// GenerateAuthToken generates a new authentication token for the given user.
|
||||
// It returns the lookup key and validator values that should be passed to the
|
||||
// user via a long-term cookie.
|
||||
func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeStamp) (lookupKey, validator string, err error) {
|
||||
func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeStamp, purpose AuthorizationPurpose) (lookupKey, validator string, err error) {
|
||||
// Request 64 random bytes. The first 32 bytes will be used for the lookupKey
|
||||
// and the other 32 bytes will be used for the validator.
|
||||
rBytes, err := util.CryptoRandomBytes(64)
|
||||
|
@ -56,14 +75,15 @@ func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeSt
|
|||
Expiry: expiry,
|
||||
LookupKey: lookupKey,
|
||||
HashedValidator: HashValidator(rBytes[32:]),
|
||||
Purpose: purpose,
|
||||
})
|
||||
return lookupKey, validator, err
|
||||
}
|
||||
|
||||
// FindAuthToken will find a authorization token via the lookup key.
|
||||
func FindAuthToken(ctx context.Context, lookupKey string) (*AuthorizationToken, error) {
|
||||
func FindAuthToken(ctx context.Context, lookupKey string, purpose AuthorizationPurpose) (*AuthorizationToken, error) {
|
||||
var authToken AuthorizationToken
|
||||
has, err := db.GetEngine(ctx).Where("lookup_key = ?", lookupKey).Get(&authToken)
|
||||
has, err := db.GetEngine(ctx).Where("lookup_key = ? AND purpose = ?", lookupKey, purpose).Get(&authToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -360,25 +359,6 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
|
|||
return MakeEmailPrimaryWithUser(ctx, user, email)
|
||||
}
|
||||
|
||||
// VerifyActiveEmailCode verifies active email code when active account
|
||||
func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
|
||||
minutes := setting.Service.ActiveCodeLives
|
||||
|
||||
if user := GetVerifyUser(ctx, code); user != nil {
|
||||
// time limit code
|
||||
prefix := code[:base.TimeLimitCodeLength]
|
||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
|
||||
|
||||
if base.VerifyTimeLimitCode(data, minutes, prefix) {
|
||||
emailAddress := &EmailAddress{UID: user.ID, Email: email}
|
||||
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
|
||||
return emailAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchEmailOrderBy is used to sort the results from SearchEmails()
|
||||
type SearchEmailOrderBy string
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@ package user
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
|
@ -308,15 +310,14 @@ func (u *User) OrganisationLink() string {
|
|||
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, nil)
|
||||
|
||||
// Add tail hex username
|
||||
code += hex.EncodeToString([]byte(u.LowerName))
|
||||
return code
|
||||
// GenerateEmailAuthorizationCode generates an activation code based for the user for the specified purpose.
|
||||
// The standard expiry is ActiveCodeLives minutes.
|
||||
func (u *User) GenerateEmailAuthorizationCode(ctx context.Context, purpose auth.AuthorizationPurpose) (string, error) {
|
||||
lookup, validator, err := auth.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(setting.Service.ActiveCodeLives)*60), purpose)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return lookup + ":" + validator, nil
|
||||
}
|
||||
|
||||
// GetUserFollowers returns range of user's followers.
|
||||
|
@ -786,38 +787,50 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
|
|||
return count
|
||||
}
|
||||
|
||||
// GetVerifyUser get user by verify code
|
||||
func GetVerifyUser(ctx context.Context, code string) (user *User) {
|
||||
if len(code) <= base.TimeLimitCodeLength {
|
||||
return nil
|
||||
// VerifyUserActiveCode verifies that the code is valid for the given purpose for this user.
|
||||
// If delete is specified, the token will be deleted.
|
||||
func VerifyUserAuthorizationToken(ctx context.Context, code string, purpose auth.AuthorizationPurpose, delete bool) (*User, error) {
|
||||
lookupKey, validator, found := strings.Cut(code, ":")
|
||||
if !found {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// use tail hex username query user
|
||||
hexStr := code[base.TimeLimitCodeLength:]
|
||||
if b, err := hex.DecodeString(hexStr); err == nil {
|
||||
if user, err = GetUserByName(ctx, string(b)); user != nil {
|
||||
return user
|
||||
authToken, err := auth.FindAuthToken(ctx, lookupKey, purpose)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
log.Error("user.getVerifyUser: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
if authToken.IsExpired() {
|
||||
return nil, auth.DeleteAuthToken(ctx, authToken)
|
||||
}
|
||||
|
||||
// VerifyUserActiveCode verifies active code when active account
|
||||
func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
|
||||
minutes := setting.Service.ActiveCodeLives
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user = GetVerifyUser(ctx, code); user != nil {
|
||||
// time limit code
|
||||
prefix := code[:base.TimeLimitCodeLength]
|
||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands)
|
||||
if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 {
|
||||
return nil, errors.New("validator doesn't match")
|
||||
}
|
||||
|
||||
if base.VerifyTimeLimitCode(data, minutes, prefix) {
|
||||
return user
|
||||
u, err := GetUserByID(ctx, authToken.UID)
|
||||
if err != nil {
|
||||
if IsErrUserNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if delete {
|
||||
if err := auth.DeleteAuthToken(ctx, authToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// ValidateUser check if user is valid to insert / update into database
|
||||
|
|
|
@ -5,8 +5,9 @@ package user_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -19,7 +20,9 @@ import (
|
|||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -543,3 +546,66 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateEmailAuthorizationCode(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 2)()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookupKey, validator, ok := strings.Cut(code, ":")
|
||||
assert.True(t, ok)
|
||||
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
require.NoError(t, err)
|
||||
|
||||
authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, authToken.IsExpired())
|
||||
assert.EqualValues(t, authToken.HashedValidator, auth.HashValidator(rawValidator))
|
||||
|
||||
authToken.Expiry = authToken.Expiry.Add(-int64(setting.Service.ActiveCodeLives) * 60)
|
||||
assert.True(t, authToken.IsExpired())
|
||||
}
|
||||
|
||||
func TestVerifyUserAuthorizationToken(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 2)()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookupKey, _, ok := strings.Cut(code, ":")
|
||||
assert.True(t, ok)
|
||||
|
||||
t.Run("Wrong purpose", func(t *testing.T) {
|
||||
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.PasswordReset, false)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, u)
|
||||
})
|
||||
|
||||
t.Run("No delete", func(t *testing.T) {
|
||||
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, false)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, user.ID, u.ID)
|
||||
|
||||
authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, authToken)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, true)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, user.ID, u.ID)
|
||||
|
||||
authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation)
|
||||
require.ErrorIs(t, err, util.ErrNotExist)
|
||||
assert.Nil(t, authToken)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,12 +15,10 @@ import (
|
|||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
@ -61,67 +59,6 @@ func BasicAuthDecode(encoded string) (string, string, error) {
|
|||
return auth[0], auth[1], nil
|
||||
}
|
||||
|
||||
// VerifyTimeLimitCode verify time limit code
|
||||
func VerifyTimeLimitCode(data string, minutes int, code string) bool {
|
||||
if len(code) <= 18 {
|
||||
return false
|
||||
}
|
||||
|
||||
// split code
|
||||
start := code[:12]
|
||||
lives := code[12:18]
|
||||
if d, err := strconv.ParseInt(lives, 10, 0); err == nil {
|
||||
minutes = int(d)
|
||||
}
|
||||
|
||||
// right active code
|
||||
retCode := CreateTimeLimitCode(data, minutes, start)
|
||||
if retCode == code && minutes > 0 {
|
||||
// check time is expired or not
|
||||
before, _ := time.ParseInLocation("200601021504", start, time.Local)
|
||||
now := time.Now()
|
||||
if before.Add(time.Minute*time.Duration(minutes)).Unix() > now.Unix() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// TimeLimitCodeLength default value for time limit code
|
||||
const TimeLimitCodeLength = 12 + 6 + 40
|
||||
|
||||
// CreateTimeLimitCode create a time limit code
|
||||
// code format: 12 length date time string + 6 minutes string + 40 sha1 encoded string
|
||||
func CreateTimeLimitCode(data string, minutes int, startInf any) string {
|
||||
format := "200601021504"
|
||||
|
||||
var start, end time.Time
|
||||
var startStr, endStr string
|
||||
|
||||
if startInf == nil {
|
||||
// Use now time create code
|
||||
start = time.Now()
|
||||
startStr = start.Format(format)
|
||||
} else {
|
||||
// use start string create code
|
||||
startStr = startInf.(string)
|
||||
start, _ = time.ParseInLocation(format, startStr, time.Local)
|
||||
startStr = start.Format(format)
|
||||
}
|
||||
|
||||
end = start.Add(time.Minute * time.Duration(minutes))
|
||||
endStr = end.Format(format)
|
||||
|
||||
// create sha1 encode string
|
||||
sh := sha1.New()
|
||||
_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, endStr, minutes)))
|
||||
encoded := hex.EncodeToString(sh.Sum(nil))
|
||||
|
||||
code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
|
||||
return code
|
||||
}
|
||||
|
||||
// FileSize calculates the file size and generate user-friendly string.
|
||||
func FileSize(s int64) string {
|
||||
return humanize.IBytes(uint64(s))
|
||||
|
|
|
@ -5,7 +5,6 @@ package base
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -45,46 +44,6 @@ func TestBasicAuthDecode(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestVerifyTimeLimitCode(t *testing.T) {
|
||||
tc := []struct {
|
||||
data string
|
||||
minutes int
|
||||
code string
|
||||
valid bool
|
||||
}{{
|
||||
data: "data",
|
||||
minutes: 2,
|
||||
code: testCreateTimeLimitCode(t, "data", 2),
|
||||
valid: true,
|
||||
}, {
|
||||
data: "abc123-ß",
|
||||
minutes: 1,
|
||||
code: testCreateTimeLimitCode(t, "abc123-ß", 1),
|
||||
valid: true,
|
||||
}, {
|
||||
data: "data",
|
||||
minutes: 2,
|
||||
code: "2021012723240000005928251dac409d2c33a6eb82c63410aaad569bed",
|
||||
valid: false,
|
||||
}}
|
||||
for _, test := range tc {
|
||||
actualValid := VerifyTimeLimitCode(test.data, test.minutes, test.code)
|
||||
assert.Equal(t, test.valid, actualValid, "data: '%s' code: '%s' should be valid: %t", test.data, test.code, test.valid)
|
||||
}
|
||||
}
|
||||
|
||||
func testCreateTimeLimitCode(t *testing.T, data string, m int) string {
|
||||
result0 := CreateTimeLimitCode(data, m, nil)
|
||||
result1 := CreateTimeLimitCode(data, m, time.Now().Format("200601021504"))
|
||||
result2 := CreateTimeLimitCode(data, m, time.Unix(time.Now().Unix()+int64(time.Minute)*int64(m), 0).Format("200601021504"))
|
||||
|
||||
assert.Equal(t, result0, result1)
|
||||
assert.NotEqual(t, result0, result2)
|
||||
|
||||
assert.NotEmpty(t, result0)
|
||||
return result0
|
||||
}
|
||||
|
||||
func TestFileSize(t *testing.T) {
|
||||
var size int64 = 512
|
||||
assert.Equal(t, "512 B", FileSize(size))
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -62,38 +60,11 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
lookupKey, validator, found := strings.Cut(authCookie, ":")
|
||||
if !found {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
authToken, err := auth.FindAuthToken(ctx, lookupKey)
|
||||
u, err := user_model.VerifyUserAuthorizationToken(ctx, authCookie, auth.LongTermAuthorization, false)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
return false, fmt.Errorf("VerifyUserAuthorizationToken: %w", err)
|
||||
}
|
||||
|
||||
if authToken.IsExpired() {
|
||||
err = auth.DeleteAuthToken(ctx, authToken)
|
||||
return false, err
|
||||
}
|
||||
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(ctx, authToken.UID)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
return false, fmt.Errorf("GetUserByID: %w", err)
|
||||
}
|
||||
if u == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
@ -635,7 +606,10 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
|
|||
return false
|
||||
}
|
||||
|
||||
mailer.SendActivateAccountMail(ctx.Locale, u)
|
||||
if err := mailer.SendActivateAccountMail(ctx, u); err != nil {
|
||||
ctx.ServerError("SendActivateAccountMail", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Data["IsSendRegisterMail"] = true
|
||||
ctx.Data["Email"] = u.Email
|
||||
|
@ -676,7 +650,10 @@ func Activate(ctx *context.Context) {
|
|||
ctx.Data["ResendLimited"] = true
|
||||
} else {
|
||||
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
|
||||
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
|
||||
if err := mailer.SendActivateAccountMail(ctx, ctx.Doer); err != nil {
|
||||
ctx.ServerError("SendActivateAccountMail", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctx.Cache.Put(cacheKey+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
||||
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
||||
|
@ -689,7 +666,12 @@ func Activate(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
user := user_model.VerifyUserActiveCode(ctx, code)
|
||||
user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, false)
|
||||
if err != nil {
|
||||
ctx.ServerError("VerifyUserAuthorizationToken", err)
|
||||
return
|
||||
}
|
||||
|
||||
// if code is wrong
|
||||
if user == nil {
|
||||
ctx.Data["IsCodeInvalid"] = true
|
||||
|
@ -753,7 +735,12 @@ func ActivatePost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
user := user_model.VerifyUserActiveCode(ctx, code)
|
||||
user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, true)
|
||||
if err != nil {
|
||||
ctx.ServerError("VerifyUserAuthorizationToken", err)
|
||||
return
|
||||
}
|
||||
|
||||
// if code is wrong
|
||||
if user == nil {
|
||||
ctx.Data["IsCodeInvalid"] = true
|
||||
|
@ -837,23 +824,32 @@ func ActivateEmail(ctx *context.Context) {
|
|||
code := ctx.FormString("code")
|
||||
emailStr := ctx.FormString("email")
|
||||
|
||||
// Verify code.
|
||||
if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil {
|
||||
if err := user_model.ActivateEmail(ctx, email); err != nil {
|
||||
ctx.ServerError("ActivateEmail", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Email activated: %s", email.Email)
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
|
||||
|
||||
if u, err := user_model.GetUserByID(ctx, email.UID); err != nil {
|
||||
log.Warn("GetUserByID: %d", email.UID)
|
||||
} else {
|
||||
// Allow user to validate more emails
|
||||
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
|
||||
}
|
||||
u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.EmailActivation(emailStr), true)
|
||||
if err != nil {
|
||||
ctx.ServerError("VerifyUserAuthorizationToken", err)
|
||||
return
|
||||
}
|
||||
if u == nil {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
|
||||
return
|
||||
}
|
||||
|
||||
email, err := user_model.GetEmailAddressOfUser(ctx, emailStr, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetEmailAddressOfUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_model.ActivateEmail(ctx, email); err != nil {
|
||||
ctx.ServerError("ActivateEmail", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Email activated: %s", email.Email)
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
|
||||
|
||||
// Allow user to validate more emails
|
||||
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
|
||||
|
||||
// FIXME: e-mail verification does not require the user to be logged in,
|
||||
// so this could be redirecting to the login page.
|
||||
|
|
|
@ -87,7 +87,10 @@ func ForgotPasswdPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
mailer.SendResetPasswordMail(u)
|
||||
if err := mailer.SendResetPasswordMail(ctx, u); err != nil {
|
||||
ctx.ServerError("SendResetPasswordMail", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
|
||||
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
||||
|
@ -98,7 +101,7 @@ func ForgotPasswdPost(ctx *context.Context) {
|
|||
ctx.HTML(http.StatusOK, tplForgotPassword)
|
||||
}
|
||||
|
||||
func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFactor) {
|
||||
func commonResetPassword(ctx *context.Context, shouldDeleteToken bool) (*user_model.User, *auth.TwoFactor) {
|
||||
code := ctx.FormString("code")
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("auth.reset_password")
|
||||
|
@ -114,7 +117,12 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
|
|||
}
|
||||
|
||||
// Fail early, don't frustrate the user
|
||||
u := user_model.VerifyUserActiveCode(ctx, code)
|
||||
u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.PasswordReset, shouldDeleteToken)
|
||||
if err != nil {
|
||||
ctx.ServerError("VerifyUserAuthorizationToken", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
|
||||
return nil, nil
|
||||
|
@ -146,7 +154,7 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
|
|||
func ResetPasswd(ctx *context.Context) {
|
||||
ctx.Data["IsResetForm"] = true
|
||||
|
||||
commonResetPassword(ctx)
|
||||
commonResetPassword(ctx, false)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
@ -156,7 +164,7 @@ func ResetPasswd(ctx *context.Context) {
|
|||
|
||||
// ResetPasswdPost response from account recovery request
|
||||
func ResetPasswdPost(ctx *context.Context) {
|
||||
u, twofa := commonResetPassword(ctx)
|
||||
u, twofa := commonResetPassword(ctx, true)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -146,9 +146,15 @@ func EmailPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
// Only fired when the primary email is inactive (Wrong state)
|
||||
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
|
||||
if err := mailer.SendActivateAccountMail(ctx, ctx.Doer); err != nil {
|
||||
ctx.ServerError("SendActivateAccountMail", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
mailer.SendActivateEmailMail(ctx.Doer, email.Email)
|
||||
if err := mailer.SendActivateEmailMail(ctx, ctx.Doer, email.Email); err != nil {
|
||||
ctx.ServerError("SendActivateEmailMail", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
address = email.Email
|
||||
|
||||
|
@ -209,7 +215,10 @@ func EmailPost(ctx *context.Context) {
|
|||
|
||||
// Send confirmation email
|
||||
if setting.Service.RegisterEmailConfirm {
|
||||
mailer.SendActivateEmailMail(ctx.Doer, form.Email)
|
||||
if err := mailer.SendActivateEmailMail(ctx, ctx.Doer, form.Email); err != nil {
|
||||
ctx.ServerError("SendActivateEmailMail", err)
|
||||
return
|
||||
}
|
||||
if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
||||
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ func (ctx *Context) GetSiteCookie(name string) string {
|
|||
// SetLTACookie will generate a LTA token and add it as an cookie.
|
||||
func (ctx *Context) SetLTACookie(u *user_model.User) error {
|
||||
days := 86400 * setting.LogInRememberDays
|
||||
lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days)))
|
||||
lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days)), auth_model.LongTermAuthorization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -227,6 +227,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
|
|||
// find redirects without existing user.
|
||||
genericOrphanCheck("Orphaned Redirects without existing redirect user",
|
||||
"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
|
||||
// find authorization tokens without existing user
|
||||
genericOrphanCheck("Authorization token without existing User",
|
||||
"forgejo_auth_token", "user", "forgejo_auth_token.uid=user.id"),
|
||||
)
|
||||
|
||||
for _, c := range consistencyChecks {
|
||||
|
|
|
@ -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"
|
||||
|
@ -64,7 +65,7 @@ func SendTestMail(email string) error {
|
|||
}
|
||||
|
||||
// sendUserMail sends a mail to the user
|
||||
func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, subject, info string) {
|
||||
func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, subject, info string) error {
|
||||
locale := translation.NewLocale(language)
|
||||
data := map[string]any{
|
||||
"locale": locale,
|
||||
|
@ -78,47 +79,66 @@ func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, s
|
|||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
msg := NewMessage(u.Email, subject, content.String())
|
||||
msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info)
|
||||
|
||||
SendAsync(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendActivateAccountMail sends an activation mail to the user (new user registration)
|
||||
func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
|
||||
func SendActivateAccountMail(ctx context.Context, u *user_model.User) error {
|
||||
if setting.MailService == nil {
|
||||
// No mail service configured
|
||||
return
|
||||
return nil
|
||||
}
|
||||
sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
|
||||
|
||||
locale := translation.NewLocale(u.Language)
|
||||
code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.UserActivation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sendUserMail(locale.Language(), u, mailAuthActivate, code, locale.TrString("mail.activate_account"), "activate account")
|
||||
}
|
||||
|
||||
// SendResetPasswordMail sends a password reset mail to the user
|
||||
func SendResetPasswordMail(u *user_model.User) {
|
||||
func SendResetPasswordMail(ctx context.Context, u *user_model.User) error {
|
||||
if setting.MailService == nil {
|
||||
// No mail service configured
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
locale := translation.NewLocale(u.Language)
|
||||
sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account")
|
||||
code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.PasswordReset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sendUserMail(u.Language, u, mailAuthResetPassword, code, locale.TrString("mail.reset_password"), "recover account")
|
||||
}
|
||||
|
||||
// SendActivateEmailMail sends confirmation email to confirm new email address
|
||||
func SendActivateEmailMail(u *user_model.User, email string) {
|
||||
func SendActivateEmailMail(ctx context.Context, u *user_model.User, email string) error {
|
||||
if setting.MailService == nil {
|
||||
// No mail service configured
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
locale := translation.NewLocale(u.Language)
|
||||
code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.EmailActivation(email))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"locale": locale,
|
||||
"DisplayName": u.DisplayName(),
|
||||
"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
|
||||
"Code": u.GenerateEmailActivateCode(email),
|
||||
"Code": code,
|
||||
"Email": email,
|
||||
"Language": locale.Language(),
|
||||
}
|
||||
|
@ -126,14 +146,14 @@ func SendActivateEmailMail(u *user_model.User, email string) {
|
|||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String())
|
||||
msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
|
||||
|
||||
SendAsync(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendRegisterNotifyMail triggers a notify e-mail by admin created a account.
|
||||
|
|
|
@ -96,6 +96,7 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
|
|||
&user_model.BlockedUser{BlockID: u.ID},
|
||||
&user_model.BlockedUser{UserID: u.ID},
|
||||
&actions_model.ActionRunnerToken{OwnerID: u.ID},
|
||||
&auth_model.AuthorizationToken{UID: u.ID},
|
||||
); err != nil {
|
||||
return fmt.Errorf("deleteBeans: %w", err)
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ func TestLTACookie(t *testing.T) {
|
|||
assert.True(t, found)
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
require.NoError(t, err)
|
||||
unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{LookupKey: lookupKey, HashedValidator: auth.HashValidator(rawValidator), UID: user.ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{LookupKey: lookupKey, HashedValidator: auth.HashValidator(rawValidator), UID: user.ID, Purpose: auth.LongTermAuthorization})
|
||||
|
||||
// Check if the LTA cookie it provides authentication.
|
||||
// If LTA cookie provides authentication /user/login shouldn't return status 200.
|
||||
|
@ -143,7 +143,7 @@ func TestLTAExpiry(t *testing.T) {
|
|||
assert.True(t, found)
|
||||
|
||||
// Ensure it's not expired.
|
||||
lta := unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
|
||||
lta := unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization})
|
||||
assert.False(t, lta.IsExpired())
|
||||
|
||||
// Manually stub LTA's expiry.
|
||||
|
@ -151,7 +151,7 @@ func TestLTAExpiry(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Ensure it's expired.
|
||||
lta = unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
|
||||
lta = unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization})
|
||||
assert.True(t, lta.IsExpired())
|
||||
|
||||
// Should return 200 OK, because LTA doesn't provide authorization anymore.
|
||||
|
@ -160,5 +160,5 @@ func TestLTAExpiry(t *testing.T) {
|
|||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Ensure it's deleted.
|
||||
unittest.AssertNotExistsBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
|
||||
unittest.AssertNotExistsBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization})
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
@ -293,8 +294,10 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
session.jar.SetCookies(baseURL, cr.Cookies())
|
||||
|
||||
activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com"))
|
||||
req = NewRequestWithValues(t, "POST", activateURL, map[string]string{
|
||||
code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation)
|
||||
require.NoError(t, err)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user/activate?code="+url.QueryEscape(code), map[string]string{
|
||||
"password": "examplePassword!1",
|
||||
})
|
||||
|
||||
|
|
|
@ -5,12 +5,16 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
|
@ -20,9 +24,11 @@ import (
|
|||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestViewUser(t *testing.T) {
|
||||
|
@ -612,3 +618,171 @@ func TestUserPronouns(t *testing.T) {
|
|||
assert.EqualValues(t, "user2", userName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserActivate(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
|
||||
|
||||
called := false
|
||||
code := ""
|
||||
defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) {
|
||||
called = true
|
||||
assert.Len(t, msgs, 1)
|
||||
assert.Equal(t, `doesnotexist@example.com`, msgs[0].To)
|
||||
assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.activate_account"), msgs[0].Subject)
|
||||
|
||||
messageDoc := NewHTMLParser(t, bytes.NewBuffer([]byte(msgs[0].Body)))
|
||||
link, ok := messageDoc.Find("a").Attr("href")
|
||||
assert.True(t, ok)
|
||||
u, err := url.Parse(link)
|
||||
require.NoError(t, err)
|
||||
code = u.Query()["code"][0]
|
||||
})()
|
||||
|
||||
session := emptyTestSession(t)
|
||||
req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/user/sign_up"),
|
||||
"user_name": "doesnotexist",
|
||||
"email": "doesnotexist@example.com",
|
||||
"password": "examplePassword!1",
|
||||
"retype": "examplePassword!1",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.True(t, called)
|
||||
|
||||
queryCode, err := url.QueryUnescape(code)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookupKey, validator, ok := strings.Cut(queryCode, ":")
|
||||
assert.True(t, ok)
|
||||
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
require.NoError(t, err)
|
||||
|
||||
authToken, err := auth_model.FindAuthToken(db.DefaultContext, lookupKey, auth_model.UserActivation)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, authToken.IsExpired())
|
||||
assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator))
|
||||
|
||||
req = NewRequest(t, "POST", "/user/activate?code="+code)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "doesnotexist", IsActive: true})
|
||||
}
|
||||
|
||||
func TestUserPasswordReset(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
called := false
|
||||
code := ""
|
||||
defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) {
|
||||
if called {
|
||||
return
|
||||
}
|
||||
called = true
|
||||
|
||||
assert.Len(t, msgs, 1)
|
||||
assert.Equal(t, user2.Email, msgs[0].To)
|
||||
assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.reset_password"), msgs[0].Subject)
|
||||
|
||||
messageDoc := NewHTMLParser(t, bytes.NewBuffer([]byte(msgs[0].Body)))
|
||||
link, ok := messageDoc.Find("a").Attr("href")
|
||||
assert.True(t, ok)
|
||||
u, err := url.Parse(link)
|
||||
require.NoError(t, err)
|
||||
code = u.Query()["code"][0]
|
||||
})()
|
||||
|
||||
session := emptyTestSession(t)
|
||||
req := NewRequestWithValues(t, "POST", "/user/forgot_password", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/user/forgot_password"),
|
||||
"email": user2.Email,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.True(t, called)
|
||||
|
||||
queryCode, err := url.QueryUnescape(code)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookupKey, validator, ok := strings.Cut(queryCode, ":")
|
||||
assert.True(t, ok)
|
||||
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
require.NoError(t, err)
|
||||
|
||||
authToken, err := auth_model.FindAuthToken(db.DefaultContext, lookupKey, auth_model.PasswordReset)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, authToken.IsExpired())
|
||||
assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user/recover_account", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/user/recover_account"),
|
||||
"code": code,
|
||||
"password": "new_password",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID})
|
||||
assert.True(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).ValidatePassword("new_password"))
|
||||
}
|
||||
|
||||
func TestActivateEmailAddress(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
called := false
|
||||
code := ""
|
||||
defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) {
|
||||
if called {
|
||||
return
|
||||
}
|
||||
called = true
|
||||
|
||||
assert.Len(t, msgs, 1)
|
||||
assert.Equal(t, "newemail@example.org", msgs[0].To)
|
||||
assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.activate_email"), msgs[0].Subject)
|
||||
|
||||
messageDoc := NewHTMLParser(t, bytes.NewBuffer([]byte(msgs[0].Body)))
|
||||
link, ok := messageDoc.Find("a").Attr("href")
|
||||
assert.True(t, ok)
|
||||
u, err := url.Parse(link)
|
||||
require.NoError(t, err)
|
||||
code = u.Query()["code"][0]
|
||||
})()
|
||||
|
||||
session := loginUser(t, user2.Name)
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings/account/email", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/user/settings"),
|
||||
"email": "newemail@example.org",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
assert.True(t, called)
|
||||
|
||||
queryCode, err := url.QueryUnescape(code)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookupKey, validator, ok := strings.Cut(queryCode, ":")
|
||||
assert.True(t, ok)
|
||||
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
require.NoError(t, err)
|
||||
|
||||
authToken, err := auth_model.FindAuthToken(db.DefaultContext, lookupKey, auth_model.EmailActivation("newemail@example.org"))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, authToken.IsExpired())
|
||||
assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user/activate_email", map[string]string{
|
||||
"code": code,
|
||||
"email": "newemail@example.org",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{UID: user2.ID, IsActivated: true, Email: "newemail@example.org"})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue