Skip to content

Commit

Permalink
Enforce 2FA
Browse files Browse the repository at this point in the history
  • Loading branch information
wxiaoguang committed Aug 27, 2022
1 parent 532c223 commit 52b1495
Show file tree
Hide file tree
Showing 13 changed files with 106 additions and 2 deletions.
4 changes: 4 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,10 @@ INTERNAL_TOKEN=
;; Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations.
;; This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
;SUCCESSFUL_TOKENS_CACHE_SIZE = 20
;;
;; Force users to enroll into Two-Factor Authentication. Users without 2FA have no access to any repositories.
;ENFORCE_TWO_FACTOR_AUTH = false


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
1 change: 1 addition & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
- off - do not check password complexity
- `PASSWORD_CHECK_PWN`: **false**: Check [HaveIBeenPwned](https://haveibeenpwned.com/Passwords) to see if a password has been exposed.
- `SUCCESSFUL_TOKENS_CACHE_SIZE`: **20**: Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations. This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
- `ENFORCE_TWO_FACTOR_AUTH`: **false**: Force users to enroll into Two-Factor Authentication. Users without 2FA have no access to any repositories.

## Camo (`camo`)

Expand Down
7 changes: 7 additions & 0 deletions models/perm/access/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,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"
)

// Access represents the highest access level of a user to the repository. The only access type
Expand All @@ -36,6 +38,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(user.ID); twoFactor == nil {
return perm.AccessModeNone, nil
}
}
userID = user.ID
restricted = user.IsRestricted
}
Expand Down
29 changes: 29 additions & 0 deletions models/perm/access/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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"
)
Expand All @@ -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})
Expand Down Expand Up @@ -66,13 +68,27 @@ func TestAccessLevel(t *testing.T) {
level, err = access_model.AccessLevel(user29, repo24)
assert.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)
assert.NoError(t, err)
assert.Equal(t, perm_model.AccessModeNone, level)

level, err = access_model.AccessLevel(user24, repo1)
assert.NoError(t, err)
assert.Equal(t, perm_model.AccessModeRead, level)
}
setting.EnforceTwoFactorAuth = false
}

func TestHasAccess(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

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)
Expand All @@ -92,6 +108,19 @@ func TestHasAccess(t *testing.T) {

_, err = access_model.HasAccess(db.DefaultContext, user2.ID, repo2)
assert.NoError(t, err)

// test enforced two-factor authentication
setting.EnforceTwoFactorAuth = true
{
has, err = access_model.HasAccess(db.DefaultContext, user1.ID, repo1)
assert.NoError(t, err)
assert.False(t, has)

has, err = access_model.HasAccess(db.DefaultContext, user24.ID, repo1)
assert.NoError(t, err)
assert.True(t, has)
}
setting.EnforceTwoFactorAuth = false
}

func TestRepository_RecalculateAccesses(t *testing.T) {
Expand Down
9 changes: 9 additions & 0 deletions models/perm/access/repo_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ 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"
repo_model "code.gitea.io/gitea/models/repo"
"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
Expand Down Expand Up @@ -168,6 +170,13 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
return
}

if user != nil && setting.EnforceTwoFactorAuth {
if twoFactor, _ := auth.GetTwoFactorByUID(user.ID); twoFactor == nil {
perm.AccessMode = perm_model.AccessModeNone
return
}
}

var is bool
if user != nil {
is, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID)
Expand Down
4 changes: 4 additions & 0 deletions modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,10 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding
ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion

ctx.Data["ShowTwoFactorRequiredMessage"] = setting.EnforceTwoFactorAuth &&
ctx.Session.Get(auth.SessionKeyUID) != nil &&
ctx.Session.Get(auth.SessionKeyTwofaAuthed) == nil

ctx.Data["EnableSwagger"] = setting.API.EnableSwagger
ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
Expand Down
2 changes: 2 additions & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ var (
PasswordHashAlgo string
PasswordCheckPwn bool
SuccessfulTokensCacheSize int
EnforceTwoFactorAuth bool

Camo = struct {
Enabled bool
Expand Down Expand Up @@ -945,6 +946,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
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 = loadInternalToken(sec)
if InstallLock && InternalToken == "" {
Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,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
Expand Down
12 changes: 12 additions & 0 deletions routers/web/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
if err := ctx.Session.Set("uname", u.Name); err != nil {
return false, err
}
if twofa, _ := auth.GetTwoFactorByUID(u.ID); twofa != nil {
if err := ctx.Session.Set(auth_service.SessionKeyTwofaAuthed, true); err != nil {
return false, err
}
}
if err := ctx.Session.Release(); err != nil {
return false, err
}
Expand Down Expand Up @@ -311,6 +316,8 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
return setting.AppSubURL + "/"
}

isTwofaAuthed := ctx.Session.Get("twofaUid") != nil

// Delete the openid, 2fa and linkaccount data
_ = ctx.Session.Delete("openid_verified_uri")
_ = ctx.Session.Delete("openid_signin_remember")
Expand All @@ -325,6 +332,11 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
if err := ctx.Session.Set("uname", u.Name); err != nil {
log.Error("Error setting uname %s session: %v", u.Name, err)
}
if isTwofaAuthed {
if err := ctx.Session.Set(auth_service.SessionKeyTwofaAuthed, true); err != nil {
log.Error("Error setting %s session: %v", auth_service.SessionKeyTwofaAuthed, err)
}
}
if err := ctx.Session.Release(); err != nil {
log.Error("Unable to store session: %v", err)
}
Expand Down
14 changes: 14 additions & 0 deletions routers/web/user/setting/security/2fa.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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/forms"

"github.com/pquerna/otp"
Expand Down Expand Up @@ -145,12 +146,21 @@ 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.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 {
// 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(auth_service.SessionKeyTwofaAuthed, true)
_ = ctx.Session.Release()
}

ctx.Redirect(setting.AppSubURL + "/user/settings/security")
return
}
Expand All @@ -171,6 +181,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.Doer.ID)
if t != nil {
Expand Down Expand Up @@ -233,6 +244,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 to 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)
Expand Down
7 changes: 7 additions & 0 deletions services/auth/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import (
"code.gitea.io/gitea/modules/log"
)

// The session keys used by different packages (in the future ...)
const (
SessionKeyUID = "uid"
SessionKeyUname = "uname"
SessionKeyTwofaAuthed = "twofaAuthed"
)

// Ensure the struct implements the interface.
var (
_ Method = &Session{}
Expand Down
5 changes: 5 additions & 0 deletions templates/base/alert.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@
<p>{{.Flash.InfoMsg | Str2html}}</p>
</div>
{{end}}
{{if .ShowTwoFactorRequiredMessage}}
<div class="ui negative message flash-error">
<p><a href="{{AppSubUrl}}/user/settings/security/two_factor/enroll">{{.locale.Tr "auth.twofa_required"}} &raquo;</a></p>
</div>
{{end}}
13 changes: 11 additions & 2 deletions templates/status/404.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@
<div class="page-content ui container center full-screen-width {{if .IsRepo}}repository{{end}}">
{{if .IsRepo}}{{template "repo/header" .}}{{end}}
<div class="ui container center">
<p style="margin-top: 100px"><img class="ui centered image" src="{{AssetUrlPrefix}}/img/404.png" alt="404"/></p>
{{if .ShowTwoFactorRequiredMessage}}
<div class="ui negative message flash-error">
<p><a href="{{AppSubUrl}}/user/settings/security/two_factor/enroll">{{.locale.Tr "auth.twofa_required"}} &raquo;</a></p>
</div>
{{end}}
<p style="margin-top: 100px;"><img class="ui centered image" src="{{AssetUrlPrefix}}/img/404.png" alt="404"/></p>
<div class="ui divider"></div>
<br>
<p>{{.locale.Tr "error404" | Safe}}
<p>
{{.locale.Tr "error404" | Safe}}
{{/* make a clear guide to tell a anonymous user should try to sign-in to access the repository, otherwise a 404 page may confuse a user who hasn't signed-in */}}
{{if not .IsSigned}}<a href="{{AppSubUrl}}/user/forget_password">{{.locale.Tr "sign_in"}} &raquo;</a>{{end}}
</p>
{{if .ShowFooterVersion}}<p>{{.locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
</div>
</div>
Expand Down

0 comments on commit 52b1495

Please sign in to comment.