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"}} »</a></p> +</div> +{{- end -}}