diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 8d39551168..614e7d078b 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -518,6 +518,9 @@ INTERNAL_TOKEN =
 ;;
 ;; On user registration, record the IP address and user agent of the user to help identify potential abuse.
 ;; RECORD_USER_SIGNUP_METADATA = false
+;;
+;; Force users to enroll into Two-Factor Authentication. Users without 2FA have no access to any repositories.
+;ENFORCE_TWO_FACTOR_AUTH = false
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/modules/session/key.go b/modules/session/key.go
new file mode 100644
index 0000000000..9f02e8f2f0
--- /dev/null
+++ b/modules/session/key.go
@@ -0,0 +1,7 @@
+package session
+
+const (
+	KeyUID            = "uid"
+	KeyUname          = "uname"
+	KeyTwofaSatisfied = "twofaSatisfied"
+)
diff --git a/modules/setting/security.go b/modules/setting/security.go
index 2f798b75c7..f1f3f8a48f 100644
--- a/modules/setting/security.go
+++ b/modules/setting/security.go
@@ -39,6 +39,7 @@ var (
 	CSRFCookieName                     = "_csrf"
 	CSRFCookieHTTPOnly                 = true
 	RecordUserSignupMetadata           = false
+	EnforceTwoFactorAuth               = false
 )
 
 // loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
@@ -141,6 +142,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
 	CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true)
 	PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
 	SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
+	EnforceTwoFactorAuth = sec.Key("ENFORCE_TWO_FACTOR_AUTH").MustBool(false)
 
 	InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
 	if InstallLock && InternalToken == "" {
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 9e6c5e61ac..0a8c9269dc 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -449,6 +449,7 @@ use_scratch_code = Use a scratch code
 twofa_scratch_used = You have used your scratch code. You have been redirected to the two-factor settings page so you may remove your device enrollment or generate a new scratch code.
 twofa_passcode_incorrect = Your passcode is incorrect. If you misplaced your device, use your scratch code to sign in.
 twofa_scratch_token_incorrect = Your scratch code is incorrect.
+twofa_required = You must setup Two-Factor Authentication to get access to repositories
 login_userpass = Sign In
 login_openid = OpenID
 oauth_signup_tab = Register New Account
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 1de8d7e8a3..edd4fb2394 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -86,10 +86,13 @@ func autoSignIn(ctx *context.Context) (bool, error) {
 
 	ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
 
+	twofa, _ := auth.GetTwoFactorByUID(ctx, u.ID)
 	if err := updateSession(ctx, nil, map[string]any{
 		// Set session IDs
 		"uid":   u.ID,
 		"uname": u.Name,
+
+		session.KeyTwofaSatisfied: twofa != nil,
 	}); err != nil {
 		return false, fmt.Errorf("unable to updateSession: %w", err)
 	}
diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go
index e5315efc74..152e6a91a7 100644
--- a/routers/web/user/setting/security/2fa.go
+++ b/routers/web/user/setting/security/2fa.go
@@ -6,6 +6,7 @@ package security
 
 import (
 	"bytes"
+	"code.gitea.io/gitea/modules/session"
 	"encoding/base64"
 	"html/template"
 	"image/png"
@@ -163,12 +164,21 @@ func EnrollTwoFactor(ctx *context.Context) {
 
 	ctx.Data["Title"] = ctx.Tr("settings")
 	ctx.Data["PageIsSettingsSecurity"] = true
+	ctx.Data["ShowTwoFactorRequiredMessage"] = false
 
 	t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
 	if t != nil {
 		// already enrolled - we should redirect back!
 		log.Warn("Trying to re-enroll %-v in twofa when already enrolled", ctx.Doer)
 		ctx.Flash.Error(ctx.Tr("settings.twofa_is_enrolled"))
+
+		if ctx.Session.Get(session.KeyTwofaSatisfied) == nil {
+			// in case a 2FA user is using an old session (the session doesn't know 2FA authed),
+			// he will be navigated to this page, we should update the session status
+			_ = ctx.Session.Set(session.KeyTwofaSatisfied, true)
+			_ = ctx.Session.Release()
+		}
+
 		ctx.Redirect(setting.AppSubURL + "/user/settings/security")
 		return
 	}
@@ -194,6 +204,7 @@ func EnrollTwoFactorPost(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
 	ctx.Data["Title"] = ctx.Tr("settings")
 	ctx.Data["PageIsSettingsSecurity"] = true
+	ctx.Data["ShowTwoFactorRequiredMessage"] = false
 
 	t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
 	if t != nil {
@@ -246,6 +257,12 @@ func EnrollTwoFactorPost(ctx *context.Context) {
 		return
 	}
 
+	newTwoFactorErr := auth.NewTwoFactor(ctx, t)
+	if newTwoFactorErr == nil {
+		if err := ctx.Session.Set(session.KeyTwofaSatisfied, true); err != nil {
+			log.Error("Unable to set %s to session: Error: %v", session.KeyTwofaSatisfied, err)
+		}
+	}
 	// Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
 	// If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
 	if err := ctx.Session.Delete("twofaSecret"); err != nil {
@@ -261,10 +278,10 @@ func EnrollTwoFactorPost(ctx *context.Context) {
 		log.Error("Unable to save changes to the session: %v", err)
 	}
 
-	if err = auth.NewTwoFactor(ctx, t); err != nil {
+	if newTwoFactorErr != nil {
 		// FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
 		// If there is a unique constraint fail we should just tolerate the error
-		ctx.ServerError("SettingsTwoFactor: Failed to save two factor", err)
+		ctx.ServerError("SettingsTwoFactor: Failed to save two factor", newTwoFactorErr)
 		return
 	}
 
diff --git a/services/context/context.go b/services/context/context.go
index 3c0ac54fc1..b1c384b6de 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -196,6 +196,10 @@ func Contexter() func(next http.Handler) http.Handler {
 
 			ctx.Data["SystemConfig"] = setting.Config()
 
+			ctx.Data["ShowTwoFactorRequiredMessage"] = setting.EnforceTwoFactorAuth &&
+				ctx.Session.Get("uid") != nil &&
+				ctx.Session.Get(session.KeyTwofaSatisfied) != true
+
 			// 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
 			ctx.Data["DisableStars"] = setting.Repository.DisableStars
diff --git a/templates/base/alert.tmpl b/templates/base/alert.tmpl
index 3f6d77a645..d882927bb4 100644
--- a/templates/base/alert.tmpl
+++ b/templates/base/alert.tmpl
@@ -18,3 +18,8 @@
 		<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
 	</div>
 {{- end -}}
+{{- if .ShowTwoFactorRequiredMessage -}}
+<div class="ui negative message flash-message flash-error">
+	<p><a href="{{AppSubUrl}}/user/settings/security/two_factor/enroll">{{ctx.Locale.Tr "auth.twofa_required"}} &raquo;</a></p>
+</div>
+{{- end -}}