diff --git a/models/auth/auth_token.go b/models/auth/auth_token.go
new file mode 100644
index 0000000000..2c3ca90734
--- /dev/null
+++ b/models/auth/auth_token.go
@@ -0,0 +1,96 @@
+// Copyright 2023 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package auth
+
+import (
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"time"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
+)
+
+// 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
+	Expiry          timeutil.TimeStamp
+}
+
+// TableName provides the real table name.
+func (AuthorizationToken) TableName() string {
+	return "forgejo_auth_token"
+}
+
+func init() {
+	db.RegisterModel(new(AuthorizationToken))
+}
+
+// IsExpired returns if the authorization token is expired.
+func (authToken *AuthorizationToken) IsExpired() bool {
+	return authToken.Expiry.AsLocalTime().Before(time.Now())
+}
+
+// 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) {
+	// 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)
+	if err != nil {
+		return "", "", err
+	}
+	hexEncoded := hex.EncodeToString(rBytes)
+	validator, lookupKey = hexEncoded[64:], hexEncoded[:64]
+
+	_, err = db.GetEngine(ctx).Insert(&AuthorizationToken{
+		UID:             userID,
+		Expiry:          expiry,
+		LookupKey:       lookupKey,
+		HashedValidator: HashValidator(rBytes[32:]),
+	})
+	return lookupKey, validator, err
+}
+
+// FindAuthToken will find a authorization token via the lookup key.
+func FindAuthToken(ctx context.Context, lookupKey string) (*AuthorizationToken, error) {
+	var authToken AuthorizationToken
+	has, err := db.GetEngine(ctx).Where("lookup_key = ?", lookupKey).Get(&authToken)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, fmt.Errorf("lookup key %q: %w", lookupKey, util.ErrNotExist)
+	}
+	return &authToken, nil
+}
+
+// DeleteAuthToken will delete the authorization token.
+func DeleteAuthToken(ctx context.Context, authToken *AuthorizationToken) error {
+	_, err := db.DeleteByBean(ctx, authToken)
+	return err
+}
+
+// DeleteAuthTokenByUser will delete all authorization tokens for the user.
+func DeleteAuthTokenByUser(ctx context.Context, userID int64) error {
+	if userID == 0 {
+		return nil
+	}
+
+	_, err := db.DeleteByBean(ctx, &AuthorizationToken{UID: userID})
+	return err
+}
+
+// HashValidator will return a hexified hashed version of the validator.
+func HashValidator(validator []byte) string {
+	h := sha256.New()
+	h.Write(validator)
+	return hex.EncodeToString(h.Sum(nil))
+}
diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go
index 2becf1b713..58f158bd17 100644
--- a/models/forgejo_migrations/migrate.go
+++ b/models/forgejo_migrations/migrate.go
@@ -41,6 +41,8 @@ var migrations = []*Migration{
 	NewMigration("Add Forgejo Blocked Users table", forgejo_v1_20.AddForgejoBlockedUser),
 	// v1 -> v2
 	NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable),
+	// v2 -> v3
+	NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable),
 }
 
 // GetCurrentDBVersion returns the current Forgejo database version.
diff --git a/models/forgejo_migrations/v1_20/v3.go b/models/forgejo_migrations/v1_20/v3.go
new file mode 100644
index 0000000000..38c29bed03
--- /dev/null
+++ b/models/forgejo_migrations/v1_20/v3.go
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+
+package forgejo_v1_20 //nolint:revive
+
+import (
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"xorm.io/xorm"
+)
+
+type AuthorizationToken struct {
+	ID              int64  `xorm:"pk autoincr"`
+	UID             int64  `xorm:"INDEX"`
+	LookupKey       string `xorm:"INDEX UNIQUE"`
+	HashedValidator string
+	Expiry          timeutil.TimeStamp
+}
+
+func (AuthorizationToken) TableName() string {
+	return "forgejo_auth_token"
+}
+
+func CreateAuthorizationTokenTable(x *xorm.Engine) error {
+	return x.Sync(new(AuthorizationToken))
+}
diff --git a/models/user/user.go b/models/user/user.go
index a3bdff33d2..39e758a043 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -386,6 +386,11 @@ func (u *User) SetPassword(passwd string) (err error) {
 		return nil
 	}
 
+	// Invalidate all authentication tokens for this user.
+	if err := auth.DeleteAuthTokenByUser(db.DefaultContext, u.ID); err != nil {
+		return err
+	}
+
 	if u.Salt, err = GetUserSalt(); err != nil {
 		return err
 	}
diff --git a/modules/context/context_cookie.go b/modules/context/context_cookie.go
index 1fd9719d31..39e3218d1b 100644
--- a/modules/context/context_cookie.go
+++ b/modules/context/context_cookie.go
@@ -4,16 +4,14 @@
 package context
 
 import (
-	"crypto/sha256"
-	"encoding/hex"
 	"net/http"
 	"strings"
 
+	auth_model "code.gitea.io/gitea/models/auth"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web/middleware"
-
-	"golang.org/x/crypto/pbkdf2"
 )
 
 const CookieNameFlash = "gitea_flash"
@@ -46,41 +44,13 @@ func (ctx *Context) GetSiteCookie(name string) string {
 	return middleware.GetSiteCookie(ctx.Req, name)
 }
 
-// GetSuperSecureCookie returns given cookie value from request header with secret string.
-func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
-	val := ctx.GetSiteCookie(name)
-	return ctx.CookieDecrypt(secret, val)
-}
-
-// CookieDecrypt returns given value from with secret string.
-func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {
-	if val == "" {
-		return "", false
-	}
-
-	text, err := hex.DecodeString(val)
+// 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)))
 	if err != nil {
-		return "", false
+		return err
 	}
-
-	key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
-	text, err = util.AESGCMDecrypt(key, text)
-	return string(text), err == nil
-}
-
-// SetSuperSecureCookie sets given cookie value to response header with secret string.
-func (ctx *Context) SetSuperSecureCookie(secret, name, value string, maxAge int) {
-	text := ctx.CookieEncrypt(secret, value)
-	ctx.SetSiteCookie(name, text, maxAge)
-}
-
-// CookieEncrypt encrypts a given value using the provided secret
-func (ctx *Context) CookieEncrypt(secret, value string) string {
-	key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
-	text, err := util.AESGCMEncrypt(key, []byte(value))
-	if err != nil {
-		panic("error encrypting cookie: " + err.Error())
-	}
-
-	return hex.EncodeToString(text)
+	ctx.SetSiteCookie(setting.CookieRememberName, lookup+":"+validator, days)
+	return nil
 }
diff --git a/modules/setting/security.go b/modules/setting/security.go
index 90f614d4cd..92caa05fad 100644
--- a/modules/setting/security.go
+++ b/modules/setting/security.go
@@ -19,7 +19,6 @@ var (
 	SecretKey                          string
 	InternalToken                      string // internal access token
 	LogInRememberDays                  int
-	CookieUserName                     string
 	CookieRememberName                 string
 	ReverseProxyAuthUser               string
 	ReverseProxyAuthEmail              string
@@ -104,7 +103,6 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
 	sec := rootCfg.Section("security")
 	InstallLock = HasInstallLock(rootCfg)
 	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
-	CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome")
 	SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
 	if SecretKey == "" {
 		// FIXME: https://github.com/go-gitea/gitea/issues/16832
diff --git a/modules/util/legacy.go b/modules/util/legacy.go
index 2ea293a2be..2d4de01949 100644
--- a/modules/util/legacy.go
+++ b/modules/util/legacy.go
@@ -4,10 +4,6 @@
 package util
 
 import (
-	"crypto/aes"
-	"crypto/cipher"
-	"crypto/rand"
-	"errors"
 	"io"
 	"os"
 )
@@ -40,52 +36,3 @@ func CopyFile(src, dest string) error {
 	}
 	return os.Chmod(dest, si.Mode())
 }
-
-// AESGCMEncrypt (from legacy package): encrypts plaintext with the given key using AES in GCM mode. should be replaced.
-func AESGCMEncrypt(key, plaintext []byte) ([]byte, error) {
-	block, err := aes.NewCipher(key)
-	if err != nil {
-		return nil, err
-	}
-
-	gcm, err := cipher.NewGCM(block)
-	if err != nil {
-		return nil, err
-	}
-
-	nonce := make([]byte, gcm.NonceSize())
-	if _, err := rand.Read(nonce); err != nil {
-		return nil, err
-	}
-
-	ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
-	return append(nonce, ciphertext...), nil
-}
-
-// AESGCMDecrypt (from legacy package): decrypts ciphertext with the given key using AES in GCM mode. should be replaced.
-func AESGCMDecrypt(key, ciphertext []byte) ([]byte, error) {
-	block, err := aes.NewCipher(key)
-	if err != nil {
-		return nil, err
-	}
-
-	gcm, err := cipher.NewGCM(block)
-	if err != nil {
-		return nil, err
-	}
-
-	size := gcm.NonceSize()
-	if len(ciphertext)-size <= 0 {
-		return nil, errors.New("ciphertext is empty")
-	}
-
-	nonce := ciphertext[:size]
-	ciphertext = ciphertext[size:]
-
-	plainText, err := gcm.Open(nil, nonce, ciphertext, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	return plainText, nil
-}
diff --git a/modules/util/legacy_test.go b/modules/util/legacy_test.go
index e732094c29..b7991bd365 100644
--- a/modules/util/legacy_test.go
+++ b/modules/util/legacy_test.go
@@ -4,8 +4,6 @@
 package util
 
 import (
-	"crypto/aes"
-	"crypto/rand"
 	"fmt"
 	"os"
 	"testing"
@@ -37,21 +35,3 @@ func TestCopyFile(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, testContent, dstContent)
 }
-
-func TestAESGCM(t *testing.T) {
-	t.Parallel()
-
-	key := make([]byte, aes.BlockSize)
-	_, err := rand.Read(key)
-	assert.NoError(t, err)
-
-	plaintext := []byte("this will be encrypted")
-
-	ciphertext, err := AESGCMEncrypt(key, plaintext)
-	assert.NoError(t, err)
-
-	decrypted, err := AESGCMDecrypt(key, ciphertext)
-	assert.NoError(t, err)
-
-	assert.Equal(t, plaintext, decrypted)
-}
diff --git a/routers/install/install.go b/routers/install/install.go
index 1c44e1f5fd..cb7818bd33 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -553,18 +553,13 @@ func SubmitInstall(ctx *context.Context) {
 			u, _ = user_model.GetUserByName(ctx, u.Name)
 		}
 
-		days := 86400 * setting.LogInRememberDays
-		ctx.SetSiteCookie(setting.CookieUserName, u.Name, days)
-
-		ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
-			setting.CookieRememberName, u.Name, days)
-
-		// Auto-login for admin
-		if err = ctx.Session.Set("uid", u.ID); err != nil {
+		if err := ctx.SetLTACookie(u); err != nil {
 			ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
 			return
 		}
-		if err = ctx.Session.Set("uname", u.Name); err != nil {
+
+		// Auto-login for admin
+		if err = ctx.Session.Set("uid", u.ID); err != nil {
 			ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
 			return
 		}
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 465077a909..884c6fe266 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -5,6 +5,8 @@
 package auth
 
 import (
+	"crypto/subtle"
+	"encoding/hex"
 	"errors"
 	"fmt"
 	"net/http"
@@ -50,21 +52,47 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
 		return false, nil
 	}
 
-	uname := ctx.GetSiteCookie(setting.CookieUserName)
-	if len(uname) == 0 {
+	authCookie := ctx.GetSiteCookie(setting.CookieRememberName)
+	if len(authCookie) == 0 {
 		return false, nil
 	}
 
 	isSucceed := false
 	defer func() {
 		if !isSucceed {
-			log.Trace("auto-login cookie cleared: %s", uname)
-			ctx.DeleteSiteCookie(setting.CookieUserName)
+			log.Trace("Auto login cookie is cleared: %s", authCookie)
 			ctx.DeleteSiteCookie(setting.CookieRememberName)
 		}
 	}()
 
-	u, err := user_model.GetUserByName(ctx, uname)
+	lookupKey, validator, found := strings.Cut(authCookie, ":")
+	if !found {
+		return false, nil
+	}
+
+	authToken, err := auth.FindAuthToken(ctx, lookupKey)
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			return false, nil
+		}
+		return false, 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("GetUserByName: %w", err)
@@ -72,17 +100,11 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
 		return false, nil
 	}
 
-	if val, ok := ctx.GetSuperSecureCookie(
-		base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); !ok || val != u.Name {
-		return false, nil
-	}
-
 	isSucceed = true
 
 	if err := updateSession(ctx, nil, map[string]any{
 		// Set session IDs
-		"uid":   u.ID,
-		"uname": u.Name,
+		"uid": authToken.UID,
 	}); err != nil {
 		return false, fmt.Errorf("unable to updateSession: %w", err)
 	}
@@ -291,10 +313,10 @@ func handleSignIn(ctx *context.Context, u *user_model.User, remember bool) {
 
 func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRedirect bool) string {
 	if remember {
-		days := 86400 * setting.LogInRememberDays
-		ctx.SetSiteCookie(setting.CookieUserName, u.Name, days)
-		ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
-			setting.CookieRememberName, u.Name, days)
+		if err := ctx.SetLTACookie(u); err != nil {
+			ctx.ServerError("GenerateAuthToken", err)
+			return setting.AppSubURL + "/"
+		}
 	}
 
 	if err := updateSession(ctx, []string{
@@ -307,8 +329,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
 		"twofaRemember",
 		"linkAccount",
 	}, map[string]any{
-		"uid":   u.ID,
-		"uname": u.Name,
+		"uid": u.ID,
 	}); err != nil {
 		ctx.ServerError("RegenerateSession", err)
 		return setting.AppSubURL + "/"
@@ -369,7 +390,6 @@ func getUserName(gothUser *goth.User) string {
 func HandleSignOut(ctx *context.Context) {
 	_ = ctx.Session.Flush()
 	_ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
-	ctx.DeleteSiteCookie(setting.CookieUserName)
 	ctx.DeleteSiteCookie(setting.CookieRememberName)
 	ctx.Csrf.DeleteCookie(ctx)
 	middleware.DeleteRedirectToCookie(ctx.Resp)
@@ -732,8 +752,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
 	log.Trace("User activated: %s", user.Name)
 
 	if err := updateSession(ctx, nil, map[string]any{
-		"uid":   user.ID,
-		"uname": user.Name,
+		"uid": user.ID,
 	}); err != nil {
 		log.Error("Unable to regenerate session for user: %-v with email: %s: %v", user, user.Email, err)
 		ctx.ServerError("ActivateUserEmail", err)
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 79f4711c26..9de56f6bd9 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -1118,8 +1118,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
 	// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
 	if !needs2FA {
 		if err := updateSession(ctx, nil, map[string]any{
-			"uid":   u.ID,
-			"uname": u.Name,
+			"uid": u.ID,
 		}); err != nil {
 			ctx.ServerError("updateSession", err)
 			return
diff --git a/routers/web/home.go b/routers/web/home.go
index ab3fbde2c9..4bcc4adcbf 100644
--- a/routers/web/home.go
+++ b/routers/web/home.go
@@ -54,8 +54,7 @@ func Home(ctx *context.Context) {
 	}
 
 	// Check auto-login.
-	uname := ctx.GetSiteCookie(setting.CookieUserName)
-	if len(uname) != 0 {
+	if len(ctx.GetSiteCookie(setting.CookieRememberName)) != 0 {
 		ctx.Redirect(setting.AppSubURL + "/user/login")
 		return
 	}
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index 5c14f3ad4b..f50c19a923 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -78,6 +78,15 @@ func AccountPost(ctx *context.Context) {
 			ctx.ServerError("UpdateUser", err)
 			return
 		}
+
+		// Re-generate LTA cookie.
+		if len(ctx.GetSiteCookie(setting.CookieRememberName)) != 0 {
+			if err := ctx.SetLTACookie(ctx.Doer); err != nil {
+				ctx.ServerError("SetLTACookie", err)
+				return
+			}
+		}
+
 		log.Trace("User password updated: %s", ctx.Doer.Name)
 		ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
 	}
diff --git a/routers/web/web.go b/routers/web/web.go
index bb10d86be7..593e56f586 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -181,7 +181,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
 
 		// Redirect to log in page if auto-signin info is provided and has not signed in.
 		if !options.SignOutRequired && !ctx.IsSigned &&
-			len(ctx.GetSiteCookie(setting.CookieUserName)) > 0 {
+			len(ctx.GetSiteCookie(setting.CookieRememberName)) > 0 {
 			if ctx.Req.URL.Path != "/user/events" {
 				middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
 			}
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 713463a3d4..4adf549204 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -76,10 +76,6 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
 	if err != nil {
 		log.Error(fmt.Sprintf("Error setting session: %v", err))
 	}
-	err = sess.Set("uname", user.Name)
-	if err != nil {
-		log.Error(fmt.Sprintf("Error setting session: %v", err))
-	}
 
 	// Language setting of the user overwrites the one previously set
 	// If the user does not have a locale set, we save the current one.
diff --git a/tests/integration/auth_token_test.go b/tests/integration/auth_token_test.go
new file mode 100644
index 0000000000..24c66ee261
--- /dev/null
+++ b/tests/integration/auth_token_test.go
@@ -0,0 +1,163 @@
+// Copyright 2023 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"encoding/hex"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+
+	"code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// GetSessionForLTACookie returns a new session with only the LTA cookie being set.
+func GetSessionForLTACookie(t *testing.T, ltaCookie *http.Cookie) *TestSession {
+	t.Helper()
+
+	ch := http.Header{}
+	ch.Add("Cookie", ltaCookie.String())
+	cr := http.Request{Header: ch}
+
+	session := emptyTestSession(t)
+	baseURL, err := url.Parse(setting.AppURL)
+	assert.NoError(t, err)
+	session.jar.SetCookies(baseURL, cr.Cookies())
+
+	return session
+}
+
+// GetLTACookieValue returns the value of the LTA cookie.
+func GetLTACookieValue(t *testing.T, sess *TestSession) string {
+	t.Helper()
+
+	rememberCookie := sess.GetCookie(setting.CookieRememberName)
+	assert.NotNil(t, rememberCookie)
+
+	cookieValue, err := url.QueryUnescape(rememberCookie.Value)
+	assert.NoError(t, err)
+
+	return cookieValue
+}
+
+// TestSessionCookie checks if the session cookie provides authentication.
+func TestSessionCookie(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	sess := loginUser(t, "user1")
+	assert.NotNil(t, sess.GetCookie(setting.SessionConfig.CookieName))
+
+	req := NewRequest(t, "GET", "/user/settings")
+	sess.MakeRequest(t, req, http.StatusOK)
+}
+
+// TestLTACookie checks if the LTA cookie that's returned is valid, exists in the database
+// and provides authentication of no session cookie is present.
+func TestLTACookie(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	sess := emptyTestSession(t)
+
+	req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
+		"_csrf":     GetCSRF(t, sess, "/user/login"),
+		"user_name": user.Name,
+		"password":  userPassword,
+		"remember":  "true",
+	})
+	sess.MakeRequest(t, req, http.StatusSeeOther)
+
+	// Checks if the database entry exist for the user.
+	ltaCookieValue := GetLTACookieValue(t, sess)
+	lookupKey, validator, found := strings.Cut(ltaCookieValue, ":")
+	assert.True(t, found)
+	rawValidator, err := hex.DecodeString(validator)
+	assert.NoError(t, err)
+	unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{LookupKey: lookupKey, HashedValidator: auth.HashValidator(rawValidator), UID: user.ID})
+
+	// Check if the LTA cookie it provides authentication.
+	// If LTA cookie provides authentication /user/login shouldn't return status 200.
+	session := GetSessionForLTACookie(t, sess.GetCookie(setting.CookieRememberName))
+	req = NewRequest(t, "GET", "/user/login")
+	session.MakeRequest(t, req, http.StatusSeeOther)
+}
+
+// TestLTAPasswordChange checks that LTA doesn't provide authentication when a
+// password change has happened and that the new LTA does provide authentication.
+func TestLTAPasswordChange(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+	sess := loginUserWithPasswordRemember(t, user.Name, userPassword, true)
+	oldRememberCookie := sess.GetCookie(setting.CookieRememberName)
+	assert.NotNil(t, oldRememberCookie)
+
+	// Make a simple password change.
+	req := NewRequestWithValues(t, "POST", "/user/settings/account", map[string]string{
+		"_csrf":        GetCSRF(t, sess, "/user/settings/account"),
+		"old_password": userPassword,
+		"password":     "password2",
+		"retype":       "password2",
+	})
+	sess.MakeRequest(t, req, http.StatusSeeOther)
+	rememberCookie := sess.GetCookie(setting.CookieRememberName)
+	assert.NotNil(t, rememberCookie)
+
+	// Check if the password really changed.
+	assert.NotEqualValues(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).Passwd, user.Passwd)
+
+	// /user/settings/account should provide with a new LTA cookie, so check for that.
+	// If LTA cookie provides authentication /user/login shouldn't return status 200.
+	session := GetSessionForLTACookie(t, rememberCookie)
+	req = NewRequest(t, "GET", "/user/login")
+	session.MakeRequest(t, req, http.StatusSeeOther)
+
+	// Check if the old LTA token is invalidated.
+	session = GetSessionForLTACookie(t, oldRememberCookie)
+	req = NewRequest(t, "GET", "/user/login")
+	session.MakeRequest(t, req, http.StatusOK)
+}
+
+// TestLTAExpiry tests that the LTA expiry works.
+func TestLTAExpiry(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+	sess := loginUserWithPasswordRemember(t, user.Name, userPassword, true)
+
+	ltaCookieValie := GetLTACookieValue(t, sess)
+	lookupKey, _, found := strings.Cut(ltaCookieValie, ":")
+	assert.True(t, found)
+
+	// Ensure it's not expired.
+	lta := unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
+	assert.False(t, lta.IsExpired())
+
+	// Manually stub LTA's expiry.
+	_, err := db.GetEngine(db.DefaultContext).ID(lta.ID).Table("forgejo_auth_token").Cols("expiry").Update(&auth.AuthorizationToken{Expiry: timeutil.TimeStampNow()})
+	assert.NoError(t, err)
+
+	// Ensure it's expired.
+	lta = unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
+	assert.True(t, lta.IsExpired())
+
+	// Should return 200 OK, because LTA doesn't provide authorization anymore.
+	session := GetSessionForLTACookie(t, sess.GetCookie(setting.CookieRememberName))
+	req := NewRequest(t, "GET", "/user/login")
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// Ensure it's deleted.
+	unittest.AssertNotExistsBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
+}
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
index 6b00f3575b..cb6269cea9 100644
--- a/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -17,6 +17,7 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"sync/atomic"
 	"testing"
@@ -299,6 +300,12 @@ func loginUser(t testing.TB, userName string) *TestSession {
 
 func loginUserWithPassword(t testing.TB, userName, password string) *TestSession {
 	t.Helper()
+
+	return loginUserWithPasswordRemember(t, userName, password, false)
+}
+
+func loginUserWithPasswordRemember(t testing.TB, userName, password string, rememberMe bool) *TestSession {
+	t.Helper()
 	req := NewRequest(t, "GET", "/user/login")
 	resp := MakeRequest(t, req, http.StatusOK)
 
@@ -307,6 +314,7 @@ func loginUserWithPassword(t testing.TB, userName, password string) *TestSession
 		"_csrf":     doc.GetCSRF(),
 		"user_name": userName,
 		"password":  password,
+		"remember":  strconv.FormatBool(rememberMe),
 	})
 	resp = MakeRequest(t, req, http.StatusSeeOther)