diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 9cb5a67172..2eff51fe98 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -529,7 +529,8 @@ INTERNAL_TOKEN =
 ;; HMAC to encode urls with, it **is required** if camo is enabled.
 ;HMAC_KEY =
 ;; Set to true to use camo for https too lese only non https urls are proxyed
-;ALLWAYS = false
+;; ALLWAYS is deprecated and will be removed in the future
+;ALWAYS = false
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go
index ba5e4959f0..3ffad035b7 100644
--- a/models/activities/repo_activity.go
+++ b/models/activities/repo_activity.go
@@ -34,6 +34,7 @@ type ActivityStats struct {
 	OpenedPRAuthorCount         int64
 	MergedPRs                   issues_model.PullRequestList
 	MergedPRAuthorCount         int64
+	ActiveIssues                issues_model.IssueList
 	OpenedIssues                issues_model.IssueList
 	OpenedIssueAuthorCount      int64
 	ClosedIssues                issues_model.IssueList
@@ -172,7 +173,7 @@ func (stats *ActivityStats) MergedPRPerc() int {
 
 // ActiveIssueCount returns total active issue count
 func (stats *ActivityStats) ActiveIssueCount() int {
-	return stats.OpenedIssueCount() + stats.ClosedIssueCount()
+	return len(stats.ActiveIssues)
 }
 
 // OpenedIssueCount returns open issue count
@@ -285,13 +286,21 @@ func (stats *ActivityStats) FillIssues(ctx context.Context, repoID int64, fromTi
 	stats.ClosedIssueAuthorCount = count
 
 	// New issues
-	sess = issuesForActivityStatement(ctx, repoID, fromTime, false, false)
+	sess = newlyCreatedIssues(ctx, repoID, fromTime)
 	sess.OrderBy("issue.created_unix ASC")
 	stats.OpenedIssues = make(issues_model.IssueList, 0)
 	if err = sess.Find(&stats.OpenedIssues); err != nil {
 		return err
 	}
 
+	// Active issues
+	sess = activeIssues(ctx, repoID, fromTime)
+	sess.OrderBy("issue.created_unix ASC")
+	stats.ActiveIssues = make(issues_model.IssueList, 0)
+	if err = sess.Find(&stats.ActiveIssues); err != nil {
+		return err
+	}
+
 	// Opened issue authors
 	sess = issuesForActivityStatement(ctx, repoID, fromTime, false, false)
 	if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("issue").Get(&count); err != nil {
@@ -317,6 +326,23 @@ func (stats *ActivityStats) FillUnresolvedIssues(ctx context.Context, repoID int
 	return sess.Find(&stats.UnresolvedIssues)
 }
 
+func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
+	sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
+		And("issue.is_pull = ?", false).                // Retain the is_pull check to exclude pull requests
+		And("issue.created_unix >= ?", fromTime.Unix()) // Include all issues created after fromTime
+
+	return sess
+}
+
+func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
+	sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
+		And("issue.is_pull = ?", false).
+		And("issue.created_unix >= ?", fromTime.Unix()).
+		Or("issue.closed_unix >= ?", fromTime.Unix())
+
+	return sess
+}
+
 func issuesForActivityStatement(ctx context.Context, repoID int64, fromTime time.Time, closed, unresolved bool) *xorm.Session {
 	sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
 		And("issue.is_closed = ?", closed)
diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go
index b4af371541..30ce0a4a03 100644
--- a/modules/httpcache/httpcache.go
+++ b/modules/httpcache/httpcache.go
@@ -76,7 +76,8 @@ func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag s
 		w.Header().Set("Etag", etag)
 	}
 	if lastModified != nil && !lastModified.IsZero() {
-		w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat))
+		// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
+		w.Header().Set("Last-Modified", lastModified.UTC().Format(http.TimeFormat))
 	}
 
 	if len(etag) > 0 {
diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go
index 6e147d76f5..2e3e6a7c42 100644
--- a/modules/httplib/serve.go
+++ b/modules/httplib/serve.go
@@ -79,6 +79,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
 	httpcache.SetCacheControlInHeader(header, duration)
 
 	if !opts.LastModified.IsZero() {
+		// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
 		header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
 	}
 }
diff --git a/modules/markup/camo.go b/modules/markup/camo.go
index e93797de2b..7e2583469d 100644
--- a/modules/markup/camo.go
+++ b/modules/markup/camo.go
@@ -38,7 +38,7 @@ func camoHandleLink(link string) string {
 	if setting.Camo.Enabled {
 		lnkURL, err := url.Parse(link)
 		if err == nil && lnkURL.IsAbs() && !strings.HasPrefix(link, setting.AppURL) &&
-			(setting.Camo.Allways || lnkURL.Scheme != "https") {
+			(setting.Camo.Always || lnkURL.Scheme != "https") {
 			return CamoEncode(link)
 		}
 	}
diff --git a/modules/markup/camo_test.go b/modules/markup/camo_test.go
index ba58835221..3c5d40afa0 100644
--- a/modules/markup/camo_test.go
+++ b/modules/markup/camo_test.go
@@ -28,7 +28,7 @@ func TestCamoHandleLink(t *testing.T) {
 		"https://image.proxy/eivin43gJwGVIjR9MiYYtFIk0mw/aHR0cDovL3Rlc3RpbWFnZXMub3JnL2ltZy5qcGc",
 		camoHandleLink("http://testimages.org/img.jpg"))
 
-	setting.Camo.Allways = true
+	setting.Camo.Always = true
 	assert.Equal(t,
 		"https://gitea.com/img.jpg",
 		camoHandleLink("https://gitea.com/img.jpg"))
diff --git a/modules/packages/composer/metadata.go b/modules/packages/composer/metadata.go
index 2c2e9ebf27..6035eae8ca 100644
--- a/modules/packages/composer/metadata.go
+++ b/modules/packages/composer/metadata.go
@@ -48,6 +48,7 @@ type Metadata struct {
 	Homepage    string            `json:"homepage,omitempty"`
 	License     Licenses          `json:"license,omitempty"`
 	Authors     []Author          `json:"authors,omitempty"`
+	Bin         []string          `json:"bin,omitempty"`
 	Autoload    map[string]any    `json:"autoload,omitempty"`
 	AutoloadDev map[string]any    `json:"autoload-dev,omitempty"`
 	Extra       map[string]any    `json:"extra,omitempty"`
diff --git a/modules/setting/camo.go b/modules/setting/camo.go
index 366e9a116c..608ecf8363 100644
--- a/modules/setting/camo.go
+++ b/modules/setting/camo.go
@@ -3,18 +3,28 @@
 
 package setting
 
-import "code.gitea.io/gitea/modules/log"
+import (
+	"strconv"
+
+	"code.gitea.io/gitea/modules/log"
+)
 
 var Camo = struct {
 	Enabled   bool
 	ServerURL string `ini:"SERVER_URL"`
 	HMACKey   string `ini:"HMAC_KEY"`
-	Allways   bool
+	Always    bool
 }{}
 
 func loadCamoFrom(rootCfg ConfigProvider) {
 	mustMapSetting(rootCfg, "camo", &Camo)
 	if Camo.Enabled {
+		oldValue := rootCfg.Section("camo").Key("ALLWAYS").MustString("")
+		if oldValue != "" {
+			log.Warn("camo.ALLWAYS is deprecated, use camo.ALWAYS instead")
+			Camo.Always, _ = strconv.ParseBool(oldValue)
+		}
+
 		if Camo.ServerURL == "" || Camo.HMACKey == "" {
 			log.Fatal(`Camo settings require "SERVER_URL" and HMAC_KEY`)
 		}
diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go
index 85832cf264..afc1091516 100644
--- a/modules/templates/util_avatar.go
+++ b/modules/templates/util_avatar.go
@@ -34,7 +34,7 @@ func AvatarHTML(src string, size int, class, name string) template.HTML {
 		name = "avatar"
 	}
 
-	return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
+	return template.HTML(`<img loading="lazy" class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
 }
 
 // Avatar renders user avatars. args: user, size (int), class (string)
diff --git a/modules/util/util.go b/modules/util/util.go
index 0444680228..dcd7cf4f29 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -225,6 +225,15 @@ func Iif[T any](condition bool, trueVal, falseVal T) T {
 	return falseVal
 }
 
+// IfZero returns "def" if "v" is a zero value, otherwise "v"
+func IfZero[T comparable](v, def T) T {
+	var zero T
+	if v == zero {
+		return def
+	}
+	return v
+}
+
 func ReserveLineBreakForTextarea(input string) string {
 	// Since the content is from a form which is a textarea, the line endings are \r\n.
 	// It's a standard behavior of HTML.
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 6b732fb121..61a820774d 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -231,7 +231,6 @@ string.desc = Z - A
 [error]
 occurred = An error occurred
 report_message = If you believe that this is a Forgejo bug, please search for issues on <a href="%s" target="_blank">Codeberg</a> or open a new issue if necessary.
-invalid_csrf = Bad Request: invalid CSRF token
 not_found = The target couldn't be found.
 network_error = Network error
 server_internal = Internal server error
diff --git a/release-notes/5372.md b/release-notes/5372.md
new file mode 100644
index 0000000000..fccb305f34
--- /dev/null
+++ b/release-notes/5372.md
@@ -0,0 +1,5 @@
+feat: [commit](https://codeberg.org/forgejo/forgejo/commit/9d3473119893ffde0ab36d98e7a0e41c5d0ba9a3) Add bin to Composer Metadata.
+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/f709de24039ab7e605d3e09e3b61240836381603) Fix wrong last modify time.
+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/2675a24649af2fff34f5c7e416d6ff78591d8d9c) Repo Activity: count new issues that were closed.
+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/526054332acb221e061d3900bba2dc6e012da52d) Fix incorrect /tokens api.
+fix: [commit](https://codeberg.org/forgejo/forgejo/commit/0cafec4c7a2faf810953e9d522faf5dc019e1522) Do not escape relative path in RPM primary index.
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 58271e1d43..4181577454 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -117,7 +117,9 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
 	xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...)
 
 	latest := pds[len(pds)-1]
-	ctx.Resp.Header().Set("Last-Modified", latest.Version.CreatedUnix.Format(http.TimeFormat))
+	// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
+	lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat)
+	ctx.Resp.Header().Set("Last-Modified", lastModifed)
 
 	ext := strings.ToLower(filepath.Ext(params.Filename))
 	if isChecksumExtension(ext) {
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index afcfbc00e3..22779e38d2 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -839,10 +839,16 @@ func EditIssue(ctx *context.APIContext) {
 	if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
 		var deadlineUnix timeutil.TimeStamp
 
-		if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
-			deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
-				23, 59, 59, 0, form.Deadline.Location())
-			deadlineUnix = timeutil.TimeStamp(deadline.Unix())
+		if form.RemoveDeadline == nil || !*form.RemoveDeadline {
+			if form.Deadline == nil {
+				ctx.Error(http.StatusBadRequest, "", "The due_date cannot be empty")
+				return
+			}
+			if !form.Deadline.IsZero() {
+				deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
+					23, 59, 59, 0, form.Deadline.Location())
+				deadlineUnix = timeutil.TimeStamp(deadline.Unix())
+			}
 		}
 
 		if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go
index b61ebac7d0..d5b20f7703 100644
--- a/routers/api/v1/user/app.go
+++ b/routers/api/v1/user/app.go
@@ -118,6 +118,10 @@ func CreateAccessToken(ctx *context.APIContext) {
 		ctx.Error(http.StatusBadRequest, "AccessTokenScope.Normalize", fmt.Errorf("invalid access token scope provided: %w", err))
 		return
 	}
+	if scope == "" {
+		ctx.Error(http.StatusBadRequest, "AccessTokenScope", "access token must have a scope")
+		return
+	}
 	t.Scope = scope
 
 	if err := auth_model.NewAccessToken(ctx, t); err != nil {
@@ -129,6 +133,7 @@ func CreateAccessToken(ctx *context.APIContext) {
 		Token:          t.Token,
 		ID:             t.ID,
 		TokenLastEight: t.TokenLastEight,
+		Scopes:         t.Scope.StringSlice(),
 	})
 }
 
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index 9f3b63698a..a082498dfd 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -395,7 +395,8 @@ func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string
 
 	ctx.Resp.Header().Set("Content-Type", contentType)
 	ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
-	ctx.Resp.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
+	// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
+	ctx.Resp.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat))
 	http.ServeFile(ctx.Resp, ctx.Req, reqFile)
 }
 
diff --git a/routers/web/web.go b/routers/web/web.go
index d174b4e251..39116b882d 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -132,6 +132,8 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) {
 			// ensure the session uid is deleted
 			_ = ctx.Session.Delete("uid")
 		}
+
+		ctx.Csrf.PrepareForSessionUser(ctx)
 	}
 }
 
diff --git a/services/context/context.go b/services/context/context.go
index c0819ab11e..91e7b1849d 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -127,10 +127,8 @@ func Contexter() func(next http.Handler) http.Handler {
 	csrfOpts := CsrfOptions{
 		Secret:         hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
 		Cookie:         setting.CSRFCookieName,
-		SetCookie:      true,
 		Secure:         setting.SessionConfig.Secure,
 		CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
-		Header:         "X-Csrf-Token",
 		CookieDomain:   setting.SessionConfig.Domain,
 		CookiePath:     setting.SessionConfig.CookiePath,
 		SameSite:       setting.SessionConfig.SameSite,
@@ -156,7 +154,7 @@ func Contexter() func(next http.Handler) http.Handler {
 			ctx.Base.AppendContextValue(WebContextKey, ctx)
 			ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
 
-			ctx.Csrf = PrepareCSRFProtector(csrfOpts, ctx)
+			ctx.Csrf = NewCSRFProtector(csrfOpts)
 
 			// Get the last flash message from cookie
 			lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash)
@@ -193,8 +191,6 @@ func Contexter() func(next http.Handler) http.Handler {
 			ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
 
 			ctx.Data["SystemConfig"] = setting.Config()
-			ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
-			ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
 
 			// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
 			ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
diff --git a/services/context/csrf.go b/services/context/csrf.go
index 57c55e6550..e0518a499b 100644
--- a/services/context/csrf.go
+++ b/services/context/csrf.go
@@ -20,64 +20,43 @@
 package context
 
 import (
-	"encoding/base32"
-	"fmt"
+	"html/template"
 	"net/http"
 	"strconv"
 	"time"
 
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
-	"code.gitea.io/gitea/modules/web/middleware"
+)
+
+const (
+	CsrfHeaderName  = "X-Csrf-Token"
+	CsrfFormName    = "_csrf"
+	CsrfErrorString = "Invalid CSRF token."
 )
 
 // CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
 type CSRFProtector interface {
-	// GetHeaderName returns HTTP header to search for token.
-	GetHeaderName() string
-	// GetFormName returns form value to search for token.
-	GetFormName() string
-	// GetToken returns the token.
-	GetToken() string
-	// Validate validates the token in http context.
+	// PrepareForSessionUser prepares the csrf protector for the current session user.
+	PrepareForSessionUser(ctx *Context)
+	// Validate validates the csrf token in http context.
 	Validate(ctx *Context)
-	// DeleteCookie deletes the cookie
+	// DeleteCookie deletes the csrf cookie
 	DeleteCookie(ctx *Context)
 }
 
 type csrfProtector struct {
 	opt CsrfOptions
-	// Token generated to pass via header, cookie, or hidden form value.
-	Token string
-	// This value must be unique per user.
-	ID string
-}
-
-// GetHeaderName returns the name of the HTTP header for csrf token.
-func (c *csrfProtector) GetHeaderName() string {
-	return c.opt.Header
-}
-
-// GetFormName returns the name of the form value for csrf token.
-func (c *csrfProtector) GetFormName() string {
-	return c.opt.Form
-}
-
-// GetToken returns the current token. This is typically used
-// to populate a hidden form in an HTML template.
-func (c *csrfProtector) GetToken() string {
-	return c.Token
+	// id must be unique per user.
+	id string
+	// token is the valid one which wil be used by end user and passed via header, cookie, or hidden form value.
+	token string
 }
 
 // CsrfOptions maintains options to manage behavior of Generate.
 type CsrfOptions struct {
 	// The global secret value used to generate Tokens.
 	Secret string
-	// HTTP header used to set and get token.
-	Header string
-	// Form value used to set and get token.
-	Form string
 	// Cookie value used to set and get token.
 	Cookie string
 	// Cookie domain.
@@ -87,103 +66,64 @@ type CsrfOptions struct {
 	CookieHTTPOnly bool
 	// SameSite set the cookie SameSite type
 	SameSite http.SameSite
-	// Key used for getting the unique ID per user.
-	SessionKey string
-	// oldSessionKey saves old value corresponding to SessionKey.
-	oldSessionKey string
-	// If true, send token via X-Csrf-Token header.
-	SetHeader bool
-	// If true, send token via _csrf cookie.
-	SetCookie bool
 	// Set the Secure flag to true on the cookie.
 	Secure bool
-	// Disallow Origin appear in request header.
-	Origin bool
-	// Cookie lifetime. Default is 0
-	CookieLifeTime int
+	// sessionKey is the key used for getting the unique ID per user.
+	sessionKey string
+	// oldSessionKey saves old value corresponding to sessionKey.
+	oldSessionKey string
 }
 
-func prepareDefaultCsrfOptions(opt CsrfOptions) CsrfOptions {
-	if opt.Secret == "" {
-		randBytes, err := util.CryptoRandomBytes(8)
-		if err != nil {
-			// this panic can be handled by the recover() in http handlers
-			panic(fmt.Errorf("failed to generate random bytes: %w", err))
-		}
-		opt.Secret = base32.StdEncoding.EncodeToString(randBytes)
-	}
-	if opt.Header == "" {
-		opt.Header = "X-Csrf-Token"
-	}
-	if opt.Form == "" {
-		opt.Form = "_csrf"
-	}
-	if opt.Cookie == "" {
-		opt.Cookie = "_csrf"
-	}
-	if opt.CookiePath == "" {
-		opt.CookiePath = "/"
-	}
-	if opt.SessionKey == "" {
-		opt.SessionKey = "uid"
-	}
-	if opt.CookieLifeTime == 0 {
-		opt.CookieLifeTime = int(CsrfTokenTimeout.Seconds())
-	}
-
-	opt.oldSessionKey = "_old_" + opt.SessionKey
-	return opt
-}
-
-func newCsrfCookie(c *csrfProtector, value string) *http.Cookie {
+func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie {
 	return &http.Cookie{
-		Name:     c.opt.Cookie,
+		Name:     opt.Cookie,
 		Value:    value,
-		Path:     c.opt.CookiePath,
-		Domain:   c.opt.CookieDomain,
-		MaxAge:   c.opt.CookieLifeTime,
-		Secure:   c.opt.Secure,
-		HttpOnly: c.opt.CookieHTTPOnly,
-		SameSite: c.opt.SameSite,
+		Path:     opt.CookiePath,
+		Domain:   opt.CookieDomain,
+		MaxAge:   int(CsrfTokenTimeout.Seconds()),
+		Secure:   opt.Secure,
+		HttpOnly: opt.CookieHTTPOnly,
+		SameSite: opt.SameSite,
 	}
 }
 
-// PrepareCSRFProtector returns a CSRFProtector to be used for every request.
-// Additionally, depending on options set, generated tokens will be sent via Header and/or Cookie.
-func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector {
-	opt = prepareDefaultCsrfOptions(opt)
-	x := &csrfProtector{opt: opt}
-
-	if opt.Origin && len(ctx.Req.Header.Get("Origin")) > 0 {
-		return x
+func NewCSRFProtector(opt CsrfOptions) CSRFProtector {
+	if opt.Secret == "" {
+		panic("CSRF secret is empty but it must be set") // it shouldn't happen because it is always set in code
 	}
+	opt.Cookie = util.IfZero(opt.Cookie, "_csrf")
+	opt.CookiePath = util.IfZero(opt.CookiePath, "/")
+	opt.sessionKey = "uid"
+	opt.oldSessionKey = "_old_" + opt.sessionKey
+	return &csrfProtector{opt: opt}
+}
 
-	x.ID = "0"
-	uidAny := ctx.Session.Get(opt.SessionKey)
-	if uidAny != nil {
+func (c *csrfProtector) PrepareForSessionUser(ctx *Context) {
+	c.id = "0"
+	if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil {
 		switch uidVal := uidAny.(type) {
 		case string:
-			x.ID = uidVal
+			c.id = uidVal
 		case int64:
-			x.ID = strconv.FormatInt(uidVal, 10)
+			c.id = strconv.FormatInt(uidVal, 10)
 		default:
 			log.Error("invalid uid type in session: %T", uidAny)
 		}
 	}
 
-	oldUID := ctx.Session.Get(opt.oldSessionKey)
-	uidChanged := oldUID == nil || oldUID.(string) != x.ID
-	cookieToken := ctx.GetSiteCookie(opt.Cookie)
+	oldUID := ctx.Session.Get(c.opt.oldSessionKey)
+	uidChanged := oldUID == nil || oldUID.(string) != c.id
+	cookieToken := ctx.GetSiteCookie(c.opt.Cookie)
 
 	needsNew := true
 	if uidChanged {
-		_ = ctx.Session.Set(opt.oldSessionKey, x.ID)
+		_ = ctx.Session.Set(c.opt.oldSessionKey, c.id)
 	} else if cookieToken != "" {
 		// If cookie token presents, reuse existing unexpired token, else generate a new one.
 		if issueTime, ok := ParseCsrfToken(cookieToken); ok {
 			dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
 			if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
-				x.Token = cookieToken
+				c.token = cookieToken
 				needsNew = false
 			}
 		}
@@ -191,42 +131,33 @@ func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector {
 
 	if needsNew {
 		// FIXME: actionId.
-		x.Token = GenerateCsrfToken(x.opt.Secret, x.ID, "POST", time.Now())
-		if opt.SetCookie {
-			cookie := newCsrfCookie(x, x.Token)
-			ctx.Resp.Header().Add("Set-Cookie", cookie.String())
-		}
+		c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now())
+		cookie := newCsrfCookie(&c.opt, c.token)
+		ctx.Resp.Header().Add("Set-Cookie", cookie.String())
 	}
 
-	if opt.SetHeader {
-		ctx.Resp.Header().Add(opt.Header, x.Token)
-	}
-	return x
+	ctx.Data["CsrfToken"] = c.token
+	ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + template.HTMLEscapeString(c.token) + `">`)
 }
 
 func (c *csrfProtector) validateToken(ctx *Context, token string) {
-	if !ValidCsrfToken(token, c.opt.Secret, c.ID, "POST", time.Now()) {
+	if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) {
 		c.DeleteCookie(ctx)
-		if middleware.IsAPIPath(ctx.Req) {
-			// currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints.
-			http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest)
-		} else {
-			ctx.Flash.Error(ctx.Tr("error.invalid_csrf"))
-			ctx.Redirect(setting.AppSubURL + "/")
-		}
+		// currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints.
+		// FIXME: distinguish what the response is for: HTML (web page) or JSON (fetch)
+		http.Error(ctx.Resp, CsrfErrorString, http.StatusBadRequest)
 	}
 }
 
 // Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
 // HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated.
-// If this validation fails, custom Error is sent in the reply.
-// If neither a header nor form value is found, http.StatusBadRequest is sent.
+// If this validation fails, http.StatusBadRequest is sent.
 func (c *csrfProtector) Validate(ctx *Context) {
-	if token := ctx.Req.Header.Get(c.GetHeaderName()); token != "" {
+	if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" {
 		c.validateToken(ctx, token)
 		return
 	}
-	if token := ctx.Req.FormValue(c.GetFormName()); token != "" {
+	if token := ctx.Req.FormValue(CsrfFormName); token != "" {
 		c.validateToken(ctx, token)
 		return
 	}
@@ -234,9 +165,7 @@ func (c *csrfProtector) Validate(ctx *Context) {
 }
 
 func (c *csrfProtector) DeleteCookie(ctx *Context) {
-	if c.opt.SetCookie {
-		cookie := newCsrfCookie(c, "")
-		cookie.MaxAge = -1
-		ctx.Resp.Header().Add("Set-Cookie", cookie.String())
-	}
+	cookie := newCsrfCookie(&c.opt, "")
+	cookie.MaxAge = -1
+	ctx.Resp.Header().Add("Set-Cookie", cookie.String())
 }
diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go
index 8a2db8670f..2cea04212a 100644
--- a/services/packages/rpm/repository.go
+++ b/services/packages/rpm/repository.go
@@ -13,7 +13,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"net/url"
 	"strings"
 	"time"
 
@@ -440,7 +439,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
 				Archive:   pd.FileMetadata.ArchiveSize,
 			},
 			Location: Location{
-				Href: fmt.Sprintf("package/%s/%s/%s/%s", url.PathEscape(pd.Package.Name), url.PathEscape(packageVersion), url.PathEscape(pd.FileMetadata.Architecture), url.PathEscape(fmt.Sprintf("%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture))),
+				Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture),
 			},
 			Format: Format{
 				License:   pd.VersionMetadata.License,
diff --git a/tests/integration/api_packages_composer_test.go b/tests/integration/api_packages_composer_test.go
index 9cdcd07e37..9d25cc4d64 100644
--- a/tests/integration/api_packages_composer_test.go
+++ b/tests/integration/api_packages_composer_test.go
@@ -37,6 +37,7 @@ func TestPackageComposer(t *testing.T) {
 	packageType := "composer-plugin"
 	packageAuthor := "Gitea Authors"
 	packageLicense := "MIT"
+	packageBin := "./bin/script"
 
 	var buf bytes.Buffer
 	archive := zip.NewWriter(&buf)
@@ -50,6 +51,9 @@ func TestPackageComposer(t *testing.T) {
 			{
 				"name": "` + packageAuthor + `"
 			}
+		],
+		"bin": [
+			"` + packageBin + `"
 		]
 	}`))
 	archive.Close()
@@ -211,6 +215,8 @@ func TestPackageComposer(t *testing.T) {
 		assert.Len(t, pkgs[0].Authors, 1)
 		assert.Equal(t, packageAuthor, pkgs[0].Authors[0].Name)
 		assert.Equal(t, "zip", pkgs[0].Dist.Type)
-		assert.Equal(t, "7b40bfd6da811b2b78deec1e944f156dbb2c747b", pkgs[0].Dist.Checksum)
+		assert.Equal(t, "4f5fa464c3cb808a1df191dbf6cb75363f8b7072", pkgs[0].Dist.Checksum)
+		assert.Len(t, pkgs[0].Bin, 1)
+		assert.Equal(t, packageBin, pkgs[0].Bin[0])
 	})
 }
diff --git a/tests/integration/api_token_test.go b/tests/integration/api_token_test.go
index 9c7bf37330..01d18ef6f1 100644
--- a/tests/integration/api_token_test.go
+++ b/tests/integration/api_token_test.go
@@ -23,10 +23,10 @@ func TestAPICreateAndDeleteToken(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 
-	newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, nil)
+	newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
 	deleteAPIAccessToken(t, newAccessToken, user)
 
-	newAccessToken = createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user, nil)
+	newAccessToken = createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
 	deleteAPIAccessToken(t, newAccessToken, user)
 }
 
@@ -72,19 +72,19 @@ func TestAPIDeleteTokensPermission(t *testing.T) {
 	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 
 	// admin can delete tokens for other users
-	createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user2, nil)
+	createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
 	req := NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-1").
 		AddBasicAuth(admin.Name)
 	MakeRequest(t, req, http.StatusNoContent)
 
 	// non-admin can delete tokens for himself
-	createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user2, nil)
+	createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
 	req = NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-2").
 		AddBasicAuth(user2.Name)
 	MakeRequest(t, req, http.StatusNoContent)
 
 	// non-admin can't delete tokens for other users
-	createAPIAccessTokenWithoutCleanUp(t, "test-key-3", user2, nil)
+	createAPIAccessTokenWithoutCleanUp(t, "test-key-3", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
 	req = NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-3").
 		AddBasicAuth(user4.Name)
 	MakeRequest(t, req, http.StatusForbidden)
@@ -520,7 +520,7 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model
 			unauthorizedScopes = append(unauthorizedScopes, cateogoryUnauthorizedScopes...)
 		}
 
-		accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, &unauthorizedScopes)
+		accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, unauthorizedScopes)
 		defer deleteAPIAccessToken(t, accessToken, user)
 
 		// Request the endpoint.  Verify that permission is denied.
@@ -532,20 +532,12 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model
 
 // createAPIAccessTokenWithoutCleanUp Create an API access token and assert that
 // creation succeeded.  The caller is responsible for deleting the token.
-func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *user_model.User, scopes *[]auth_model.AccessTokenScope) api.AccessToken {
+func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *user_model.User, scopes []auth_model.AccessTokenScope) api.AccessToken {
 	payload := map[string]any{
-		"name": tokenName,
-	}
-	if scopes != nil {
-		for _, scope := range *scopes {
-			scopes, scopesExists := payload["scopes"].([]string)
-			if !scopesExists {
-				scopes = make([]string, 0)
-			}
-			scopes = append(scopes, string(scope))
-			payload["scopes"] = scopes
-		}
+		"name":   tokenName,
+		"scopes": scopes,
 	}
+
 	log.Debug("Requesting creation of token with scopes: %v", scopes)
 	req := NewRequestWithJSON(t, "POST", "/api/v1/users/"+user.LoginName+"/tokens", payload).
 		AddBasicAuth(user.Name)
@@ -563,8 +555,7 @@ func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *us
 	return newAccessToken
 }
 
-// createAPIAccessTokenWithoutCleanUp Delete an API access token and assert that
-// deletion succeeded.
+// deleteAPIAccessToken deletes an API access token and assert that deletion succeeded.
 func deleteAPIAccessToken(t *testing.T, accessToken api.AccessToken, user *user_model.User) {
 	req := NewRequestf(t, "DELETE", "/api/v1/users/"+user.LoginName+"/tokens/%d", accessToken.ID).
 		AddBasicAuth(user.Name)
diff --git a/tests/integration/attachment_test.go b/tests/integration/attachment_test.go
index 95c9c9f753..7cbc2545d5 100644
--- a/tests/integration/attachment_test.go
+++ b/tests/integration/attachment_test.go
@@ -60,7 +60,8 @@ func createAttachment(t *testing.T, session *TestSession, repoURL, filename stri
 func TestCreateAnonymousAttachment(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	session := emptyTestSession(t)
-	createAttachment(t, session, "user2/repo1", "image.png", generateImg(), http.StatusSeeOther)
+	// this test is not right because it just doesn't pass the CSRF validation
+	createAttachment(t, session, "user2/repo1", "image.png", generateImg(), http.StatusBadRequest)
 }
 
 func TestCreateIssueAttachment(t *testing.T) {
diff --git a/tests/integration/csrf_test.go b/tests/integration/csrf_test.go
index a789859889..fcb9661b8a 100644
--- a/tests/integration/csrf_test.go
+++ b/tests/integration/csrf_test.go
@@ -5,12 +5,10 @@ package integration
 
 import (
 	"net/http"
-	"strings"
 	"testing"
 
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
@@ -25,28 +23,12 @@ func TestCsrfProtection(t *testing.T) {
 	req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
 		"_csrf": "fake_csrf",
 	})
-	session.MakeRequest(t, req, http.StatusSeeOther)
-
-	resp := session.MakeRequest(t, req, http.StatusSeeOther)
-	loc := resp.Header().Get("Location")
-	assert.Equal(t, setting.AppSubURL+"/", loc)
-	resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK)
-	htmlDoc := NewHTMLParser(t, resp.Body)
-	assert.Equal(t, "Bad Request: invalid CSRF token",
-		strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
-	)
+	resp := session.MakeRequest(t, req, http.StatusBadRequest)
+	assert.Contains(t, resp.Body.String(), "Invalid CSRF token")
 
 	// test web form csrf via header. TODO: should use an UI api to test
 	req = NewRequest(t, "POST", "/user/settings")
 	req.Header.Add("X-Csrf-Token", "fake_csrf")
-	session.MakeRequest(t, req, http.StatusSeeOther)
-
-	resp = session.MakeRequest(t, req, http.StatusSeeOther)
-	loc = resp.Header().Get("Location")
-	assert.Equal(t, setting.AppSubURL+"/", loc)
-	resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK)
-	htmlDoc = NewHTMLParser(t, resp.Body)
-	assert.Equal(t, "Bad Request: invalid CSRF token",
-		strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
-	)
+	resp = session.MakeRequest(t, req, http.StatusBadRequest)
+	assert.Contains(t, resp.Body.String(), "Invalid CSRF token")
 }
diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go
index 68d7008e02..e9ad933b24 100644
--- a/tests/integration/links_test.go
+++ b/tests/integration/links_test.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
+	forgejo_context "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
@@ -190,11 +191,6 @@ func TestRedirectsWebhooks(t *testing.T) {
 			{from: "/user/settings/hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
 			{from: "/admin/system-hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
 			{from: "/admin/default-hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
-			{from: "/user2/repo1/settings/hooks/" + kind + "/new", to: "/", verb: "POST"},
-			{from: "/admin/system-hooks/" + kind + "/new", to: "/", verb: "POST"},
-			{from: "/admin/default-hooks/" + kind + "/new", to: "/", verb: "POST"},
-			{from: "/user2/repo1/settings/hooks/1", to: "/", verb: "POST"},
-			{from: "/admin/hooks/1", to: "/", verb: "POST"},
 		}
 		for _, info := range redirects {
 			req := NewRequest(t, info.verb, info.from)
@@ -202,6 +198,24 @@ func TestRedirectsWebhooks(t *testing.T) {
 			assert.EqualValues(t, path.Join(setting.AppSubURL, info.to), test.RedirectURL(resp), info.from)
 		}
 	}
+
+	for _, kind := range []string{"forgejo", "gitea"} {
+		csrf := []struct {
+			from string
+			verb string
+		}{
+			{from: "/user2/repo1/settings/hooks/" + kind + "/new", verb: "POST"},
+			{from: "/admin/hooks/1", verb: "POST"},
+			{from: "/admin/system-hooks/" + kind + "/new", verb: "POST"},
+			{from: "/admin/default-hooks/" + kind + "/new", verb: "POST"},
+			{from: "/user2/repo1/settings/hooks/1", verb: "POST"},
+		}
+		for _, info := range csrf {
+			req := NewRequest(t, info.verb, info.from)
+			resp := MakeRequest(t, req, http.StatusBadRequest)
+			assert.Contains(t, resp.Body.String(), forgejo_context.CsrfErrorString)
+		}
+	}
 }
 
 func TestRepoLinks(t *testing.T) {
diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go
index 0d5e9a0472..f385b99e46 100644
--- a/tests/integration/oauth_test.go
+++ b/tests/integration/oauth_test.go
@@ -11,6 +11,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"net/http/httptest"
 	"net/url"
 	"strings"
 	"testing"
@@ -24,6 +25,7 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/routers/web/auth"
+	forgejo_context "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/markbates/goth"
@@ -803,6 +805,16 @@ func TestOAuthIntrospection(t *testing.T) {
 	})
 }
 
+func requireCookieCSRF(t *testing.T, resp http.ResponseWriter) string {
+	for _, c := range resp.(*httptest.ResponseRecorder).Result().Cookies() {
+		if c.Name == "_csrf" {
+			return c.Value
+		}
+	}
+	require.True(t, false, "_csrf not found in cookies")
+	return ""
+}
+
 func TestOAuth_GrantScopesReadUser(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
@@ -840,19 +852,18 @@ func TestOAuth_GrantScopesReadUser(t *testing.T) {
 	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 
 	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
-	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
-		"_csrf":        htmlDoc.GetCSRF(),
+		"_csrf":        requireCookieCSRF(t, authorizeResp),
 		"client_id":    app.ClientID,
 		"redirect_uri": "a",
 		"state":        "thestate",
 		"granted":      "true",
 	})
-	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
-	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
+	grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
+	assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
 
 	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
-		"_csrf":         htmlDocGrant.GetCSRF(),
+		"_csrf":         requireCookieCSRF(t, authorizeResp),
 		"grant_type":    "authorization_code",
 		"client_id":     app.ClientID,
 		"client_secret": app.ClientSecret,
@@ -921,19 +932,18 @@ func TestOAuth_GrantScopesFailReadRepository(t *testing.T) {
 	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 
 	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
-	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
-		"_csrf":        htmlDoc.GetCSRF(),
+		"_csrf":        requireCookieCSRF(t, authorizeResp),
 		"client_id":    app.ClientID,
 		"redirect_uri": "a",
 		"state":        "thestate",
 		"granted":      "true",
 	})
-	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
-	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
+	grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
+	assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
 
 	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
-		"_csrf":         htmlDocGrant.GetCSRF(),
+		"_csrf":         requireCookieCSRF(t, authorizeResp),
 		"grant_type":    "authorization_code",
 		"client_id":     app.ClientID,
 		"client_secret": app.ClientSecret,
@@ -1000,19 +1010,18 @@ func TestOAuth_GrantScopesReadRepository(t *testing.T) {
 	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 
 	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
-	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
-		"_csrf":        htmlDoc.GetCSRF(),
+		"_csrf":        requireCookieCSRF(t, authorizeResp),
 		"client_id":    app.ClientID,
 		"redirect_uri": "a",
 		"state":        "thestate",
 		"granted":      "true",
 	})
-	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
-	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
+	grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
+	assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
 
 	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
-		"_csrf":         htmlDocGrant.GetCSRF(),
+		"_csrf":         requireCookieCSRF(t, authorizeResp),
 		"grant_type":    "authorization_code",
 		"client_id":     app.ClientID,
 		"client_secret": app.ClientSecret,
@@ -1082,19 +1091,18 @@ func TestOAuth_GrantScopesReadPrivateGroups(t *testing.T) {
 	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 
 	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
-	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
-		"_csrf":        htmlDoc.GetCSRF(),
+		"_csrf":        requireCookieCSRF(t, authorizeResp),
 		"client_id":    app.ClientID,
 		"redirect_uri": "a",
 		"state":        "thestate",
 		"granted":      "true",
 	})
-	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
-	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
+	grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
+	assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
 
 	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
-		"_csrf":         htmlDocGrant.GetCSRF(),
+		"_csrf":         requireCookieCSRF(t, authorizeResp),
 		"grant_type":    "authorization_code",
 		"client_id":     app.ClientID,
 		"client_secret": app.ClientSecret,
@@ -1164,19 +1172,18 @@ func TestOAuth_GrantScopesReadOnlyPublicGroups(t *testing.T) {
 	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 
 	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
-	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
-		"_csrf":        htmlDoc.GetCSRF(),
+		"_csrf":        requireCookieCSRF(t, authorizeResp),
 		"client_id":    app.ClientID,
 		"redirect_uri": "a",
 		"state":        "thestate",
 		"granted":      "true",
 	})
-	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
-	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
+	grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
+	assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
 
 	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
-		"_csrf":         htmlDocGrant.GetCSRF(),
+		"_csrf":         requireCookieCSRF(t, authorizeResp),
 		"grant_type":    "authorization_code",
 		"client_id":     app.ClientID,
 		"client_secret": app.ClientSecret,
@@ -1260,19 +1267,18 @@ func TestOAuth_GrantScopesReadPublicGroupsWithTheReadScope(t *testing.T) {
 	authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
 
 	authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
-	htmlDoc := NewHTMLParser(t, authorizeResp.Body)
 	grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
-		"_csrf":        htmlDoc.GetCSRF(),
+		"_csrf":        requireCookieCSRF(t, authorizeResp),
 		"client_id":    app.ClientID,
 		"redirect_uri": "a",
 		"state":        "thestate",
 		"granted":      "true",
 	})
-	grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
-	htmlDocGrant := NewHTMLParser(t, grantResp.Body)
+	grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
+	assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
 
 	accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
-		"_csrf":         htmlDocGrant.GetCSRF(),
+		"_csrf":         requireCookieCSRF(t, authorizeResp),
 		"grant_type":    "authorization_code",
 		"client_id":     app.ClientID,
 		"client_secret": app.ClientSecret,
diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go
index 2aa299479a..df9ea9a97c 100644
--- a/tests/integration/repo_branch_test.go
+++ b/tests/integration/repo_branch_test.go
@@ -18,7 +18,6 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/graceful"
-	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
 	repo_service "code.gitea.io/gitea/services/repository"
@@ -157,15 +156,8 @@ func TestCreateBranchInvalidCSRF(t *testing.T) {
 		"_csrf":           "fake_csrf",
 		"new_branch_name": "test",
 	})
-	resp := session.MakeRequest(t, req, http.StatusSeeOther)
-	loc := resp.Header().Get("Location")
-	assert.Equal(t, setting.AppSubURL+"/", loc)
-	resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK)
-	htmlDoc := NewHTMLParser(t, resp.Body)
-	assert.Equal(t,
-		"Bad Request: invalid CSRF token",
-		strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
-	)
+	resp := session.MakeRequest(t, req, http.StatusBadRequest)
+	assert.Contains(t, resp.Body.String(), "Invalid CSRF token")
 }
 
 func TestDatabaseMissingABranch(t *testing.T) {