Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:mia
forgejo
0001-Add-setting-to-enforce-two-factor-auth.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File 0001-Add-setting-to-enforce-two-factor-auth.patch of Package forgejo
From 571fac549d5797e9235bc744c411707385eaa6a7 Mon Sep 17 00:00:00 2001 From: Mia Herkt <mia@0x0.st> Date: Tue, 12 Nov 2024 03:11:22 +0100 Subject: [PATCH] Add setting to enforce two-factor auth --- custom/conf/app.example.ini | 3 +++ models/perm/access/access.go | 7 ++++++ models/perm/access/access_test.go | 29 ++++++++++++++++++++++++ models/perm/access/repo_permission.go | 9 ++++++++ modules/setting/security.go | 2 ++ options/locale/locale_de-DE.ini | 1 + options/locale/locale_en-US.ini | 1 + options/locale/locale_ja-JP.ini | 1 + routers/web/auth/auth.go | 7 ++++++ routers/web/user/setting/security/2fa.go | 13 +++++++++++ services/auth/session.go | 5 ++++ services/context/context.go | 4 ++++ templates/base/alert.tmpl | 5 ++++ 13 files changed, 87 insertions(+) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 2eff51fe98..e1f1fbc422 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -513,6 +513,9 @@ INTERNAL_TOKEN = ;; stemming from cached/logged plain-text API tokens. ;; In future releases, this will become the default behavior ;DISABLE_QUERY_AUTH_TOKEN = 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/models/perm/access/access.go b/models/perm/access/access.go index 3e2568b4b4..2d7ea55776 100644 --- a/models/perm/access/access.go +++ b/models/perm/access/access.go @@ -8,11 +8,13 @@ import ( "context" "fmt" + "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" "xorm.io/builder" ) @@ -37,6 +39,11 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re restricted := false if user != nil { + if setting.EnforceTwoFactorAuth { + if twoFactor, _ := auth.GetTwoFactorByUID(ctx, user.ID); twoFactor == nil { + return perm.AccessModeNone, nil + } + } userID = user.ID restricted = user.IsRestricted } diff --git a/models/perm/access/access_test.go b/models/perm/access/access_test.go index 556f51311c..4f970d1380 100644 --- a/models/perm/access/access_test.go +++ b/models/perm/access/access_test.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,6 +23,7 @@ func TestAccessLevel(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + user24 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 24}) user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) // A public repository owned by User 2 repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -66,6 +68,19 @@ func TestAccessLevel(t *testing.T) { level, err = access_model.AccessLevel(db.DefaultContext, user29, repo24) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, level) + + // test enforced two-factor authentication + setting.EnforceTwoFactorAuth = true + { + level, err = access_model.AccessLevel(user2, repo1) + require.NoError(t, err) + assert.Equal(t, perm_model.AccessModeNone, level) + + level, err = access_model.AccessLevel(user24, repo1) + require.NoError(t, err) + assert.Equal(t, perm_model.AccessModeRead, level) + } + setting.EnforceTwoFactorAuth = false } func TestHasAccess(t *testing.T) { @@ -73,6 +88,7 @@ func TestHasAccess(t *testing.T) { user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + user24 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 24}) // A public repository owned by User 2 repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) assert.False(t, repo1.IsPrivate) @@ -92,6 +108,19 @@ func TestHasAccess(t *testing.T) { _, err = access_model.HasAccess(db.DefaultContext, user2.ID, repo2) require.NoError(t, err) + + // test enforced two-factor authentication + setting.EnforceTwoFactorAuth = true + { + has, err = access_model.HasAccess(db.DefaultContext, user1.ID, repo1) + require.NoError(t, err) + assert.False(t, has) + + has, err = access_model.HasAccess(db.DefaultContext, user24.ID, repo1) + require.NoError(t, err) + assert.True(t, has) + } + setting.EnforceTwoFactorAuth = false } func TestRepository_RecalculateAccesses(t *testing.T) { diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 7e39627a75..0262c3821f 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -7,6 +7,7 @@ import ( "context" "fmt" + "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" perm_model "code.gitea.io/gitea/models/perm" @@ -14,6 +15,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" ) // Permission contains all the permissions related variables to a repository for a user @@ -161,6 +163,13 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use return perm, nil } + if user != nil && setting.EnforceTwoFactorAuth { + if twoFactor, _ := auth.GetTwoFactorByUID(ctx, user.ID); twoFactor == nil { + perm.AccessMode = perm_model.AccessModeNone + return perm, nil + } + } + var isCollaborator bool var err error if user != nil { diff --git a/modules/setting/security.go b/modules/setting/security.go index 678a57cb30..39935661d0 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -36,6 +36,7 @@ var ( PasswordCheckPwn bool SuccessfulTokensCacheSize int DisableQueryAuthToken bool + EnforceTwoFactorAuth bool CSRFCookieName = "_csrf" CSRFCookieHTTPOnly = true ) @@ -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_de-DE.ini b/options/locale/locale_de-DE.ini index 77a90e0fe4..69b48e33b5 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -434,6 +434,7 @@ use_scratch_code=Einmalpasswort verwenden twofa_scratch_used=Du hast dein Einmalpasswort verwendet. Du wurdest zu den Einstellung der Zwei-Faktor-Authentifizierung umgeleitet, dort kannst du dein Gerät abmelden oder ein neues Einmalpasswort erzeugen. twofa_passcode_incorrect=Ungültige PIN. Wenn du dein Gerät verloren hast, verwende dein Einmalpasswort. twofa_scratch_token_incorrect=Das Einmalpasswort ist falsch. +twofa_required = Bitte richte Zwei-Faktor-Authentifizierung ein, um auf Repositorys zugreifen zu können. login_userpass=Anmelden tab_openid=OpenID oauth_signup_tab=Neues Konto registrieren diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 661dafd235..f3f9706337 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -443,6 +443,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 = Please configure Two-Factor Authentication in order to get access to repositories. login_userpass = Sign in oauth_signup_tab = Register new account oauth_signup_title = Complete new account diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 3e90125042..0dec7032a8 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -435,6 +435,7 @@ use_scratch_code=スクラッチコードを使う twofa_scratch_used=あなたはスクラッチコードを使用しました。 2要素認証の設定ページにリダイレクトしましたので、デバイスの登録を解除するか、新しいスクラッチコードを生成しましょう。 twofa_passcode_incorrect=パスコードが正しくありません。デバイスを紛失した場合は、スクラッチコードを使ってサインインしてください。 twofa_scratch_token_incorrect=スクラッチコードが正しくありません。 +twofa_required=リポジトリにアクセスするには、2要素認証を設定してください。 login_userpass=サインイン tab_openid=OpenID oauth_signup_tab=新規アカウント登録 diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index bb20309030..e5f00ce6b6 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -99,9 +99,12 @@ func autoSignIn(ctx *context.Context) (bool, error) { isSucceed = true + twofa, _ := auth.GetTwoFactorByUID(ctx, u.ID) if err := updateSession(ctx, nil, map[string]any{ // Set session IDs "uid": u.ID, + + auth_service.SessionKeyTwofaAuthed: twofa != nil, }); err != nil { return false, fmt.Errorf("unable to updateSession: %w", err) } @@ -329,6 +332,8 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe } } + isTwofaAuthed := ctx.Session.Get("twofaUid") != nil + if err := updateSession(ctx, []string{ // Delete the openid, 2fa and linkaccount data "openid_verified_uri", @@ -340,6 +345,8 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe "linkAccount", }, map[string]any{ "uid": u.ID, + + auth_service.SessionKeyTwofaAuthed: isTwofaAuthed, }); err != nil { ctx.ServerError("RegenerateSession", err) return setting.AppSubURL + "/" diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go index 7c85c0e4b7..fab76bf082 100644 --- a/routers/web/user/setting/security/2fa.go +++ b/routers/web/user/setting/security/2fa.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" + auth_service "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/mailer" @@ -153,12 +154,20 @@ func twofaGenerateSecretAndQr(ctx *context.Context) bool { 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(auth_service.SessionKeyTwofaAuthed) == nil { + // old session that doesn't track 2FA state got navigated here + _ = ctx.Session.Set(auth_service.SessionKeyTwofaAuthed, true) + _ = ctx.Session.Release() + } + ctx.Redirect(setting.AppSubURL + "/user/settings/security") return } @@ -179,6 +188,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 { @@ -241,6 +251,9 @@ func EnrollTwoFactorPost(ctx *context.Context) { // tolerate this failure - it's more important to continue log.Error("Unable to delete twofaUri from the session: Error: %v", err) } + if err := ctx.Session.Set(auth_service.SessionKeyTwofaAuthed, true); err != nil { + log.Error("Unable to set %s for session: Error: %v", auth_service.SessionKeyTwofaAuthed, err) + } if err := ctx.Session.Release(); err != nil { // tolerate this failure - it's more important to continue log.Error("Unable to save changes to the session: %v", err) diff --git a/services/auth/session.go b/services/auth/session.go index 35d97e42da..c0939519cc 100644 --- a/services/auth/session.go +++ b/services/auth/session.go @@ -10,6 +10,11 @@ import ( "code.gitea.io/gitea/modules/log" ) +const ( + SessionKeyUID = "uid" + SessionKeyTwofaAuthed = "twofaAuthed" +) + // Ensure the struct implements the interface. var ( _ Method = &Session{} diff --git a/services/context/context.go b/services/context/context.go index 91e7b1849d..81407588d3 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -192,6 +192,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("twofaAuthed") != 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 e2853d3dab..e8e8ee763d 100644 --- a/templates/base/alert.tmpl +++ b/templates/base/alert.tmpl @@ -21,3 +21,8 @@ {{if and (not .Flash.ErrorMsg) (not .Flash.SuccessMsg) (not .Flash.InfoMsg) (not .Flash.WarningMsg) (not .IsHTMX)}} <div id="flash-message" hx-swap-oob="true"></div> {{end}} +{{if .ShowTwoFactorRequiredMessage}} + <div class="ui negative message flash-error"> + <p><a href="{{AppSubUrl}}/user/settings/security/two_factor/enroll">{{ctx.Locale.Tr "auth.twofa_required"}} »</a></p> + </div> +{{end}} -- 2.47.0
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor