Skip to content

Commit be9d45b

Browse files
committed
Backport enhanced auth token / remember me from v1.22
Without increasing database version. From upstream go-gitea#27606 The mechanism responsible for long-term authentication (the 'remember me' cookie) uses a weak construction technique. It will hash the user's hashed password and the rands value; it will then call the secure cookie code, which will encrypt the user's name with the computed hash. If one were able to dump the database, they could extract those two values to rebuild that cookie and impersonate a user. That vulnerability exists from the date the dump was obtained until a user changed their password. To fix this security issue, the cookie could be created and verified using a different technique such as the one explained at https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#secure-remember-me-cookies. The PR removes the now obsolete setting `COOKIE_USERNAME`.
1 parent c10a323 commit be9d45b

22 files changed

+409
-103
lines changed

docs/content/administration/config-cheat-sheet.en-us.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,6 @@ And the following unique queues:
517517
- `SECRET_KEY`: **\<random at every install\>**: Global secret key. This key is VERY IMPORTANT, if you lost it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
518518
- `SECRET_KEY_URI`: **_empty_**: Instead of defining SECRET_KEY, this option can be used to use the key stored in a file (example value: `file:/etc/gitea/secret_key`). It shouldn't be lost like SECRET_KEY.
519519
- `LOGIN_REMEMBER_DAYS`: **7**: Cookie lifetime, in days.
520-
- `COOKIE_USERNAME`: **gitea\_awesome**: Name of the cookie used to store the current username.
521520
- `COOKIE_REMEMBER_NAME`: **gitea\_incredible**: Name of cookie used to store authentication
522521
information.
523522
- `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**: Header name for reverse proxy

docs/content/administration/config-cheat-sheet.zh-cn.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,6 @@ Gitea 创建以下非唯一队列:
506506
- `SECRET_KEY`: **\<每次安装时随机生成\>**:全局服务器安全密钥。这个密钥非常重要,如果丢失将无法解密加密的数据(例如 2FA)。
507507
- `SECRET_KEY_URI`: **_empty_**:与定义 `SECRET_KEY` 不同,此选项可用于使用存储在文件中的密钥(示例值:`file:/etc/gitea/secret_key`)。它不应该像 `SECRET_KEY` 一样容易丢失。
508508
- `LOGIN_REMEMBER_DAYS`: **7**:Cookie 保存时间,单位为天。
509-
- `COOKIE_USERNAME`: **gitea\_awesome**:保存用户名的 Cookie 名称。
510509
- `COOKIE_REMEMBER_NAME`: **gitea\_incredible**:保存自动登录信息的 Cookie 名称。
511510
- `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**:反向代理认证的 HTTP 头部名称,用于提供用户信息。
512511
- `REVERSE_PROXY_AUTHENTICATION_EMAIL`: **X-WEBAUTH-EMAIL**:反向代理认证的 HTTP 头部名称,用于提供邮箱信息。
File renamed without changes.
File renamed without changes.
File renamed without changes.

models/auth/auth_token.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package auth
5+
6+
import (
7+
"context"
8+
9+
"code.gitea.io/gitea/models/db"
10+
"code.gitea.io/gitea/modules/timeutil"
11+
"code.gitea.io/gitea/modules/util"
12+
13+
"xorm.io/builder"
14+
)
15+
16+
var ErrAuthTokenNotExist = util.NewNotExistErrorf("auth token does not exist")
17+
18+
type AuthToken struct { //nolint:revive
19+
ID string `xorm:"pk"`
20+
TokenHash string
21+
UserID int64 `xorm:"INDEX"`
22+
ExpiresUnix timeutil.TimeStamp `xorm:"INDEX"`
23+
}
24+
25+
func init() {
26+
db.RegisterModel(new(AuthToken))
27+
}
28+
29+
func InsertAuthToken(ctx context.Context, t *AuthToken) error {
30+
_, err := db.GetEngine(ctx).Insert(t)
31+
return err
32+
}
33+
34+
func GetAuthTokenByID(ctx context.Context, id string) (*AuthToken, error) {
35+
at := &AuthToken{}
36+
37+
has, err := db.GetEngine(ctx).ID(id).Get(at)
38+
if err != nil {
39+
return nil, err
40+
}
41+
if !has {
42+
return nil, ErrAuthTokenNotExist
43+
}
44+
return at, nil
45+
}
46+
47+
func UpdateAuthTokenByID(ctx context.Context, t *AuthToken) error {
48+
_, err := db.GetEngine(ctx).ID(t.ID).Cols("token_hash", "expires_unix").Update(t)
49+
return err
50+
}
51+
52+
func DeleteAuthTokenByID(ctx context.Context, id string) error {
53+
_, err := db.GetEngine(ctx).ID(id).Delete(&AuthToken{})
54+
return err
55+
}
56+
57+
func DeleteExpiredAuthTokens(ctx context.Context) error {
58+
_, err := db.GetEngine(ctx).Where(builder.Lt{"expires_unix": timeutil.TimeStampNow()}).Delete(&AuthToken{})
59+
return err
60+
}

models/migrations/migrations.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"code.gitea.io/gitea/modules/git"
2828
"code.gitea.io/gitea/modules/log"
2929
"code.gitea.io/gitea/modules/setting"
30+
"code.gitea.io/gitea/modules/timeutil"
3031

3132
"xorm.io/xorm"
3233
"xorm.io/xorm/names"
@@ -649,5 +650,18 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
649650
return err
650651
}
651652
}
653+
654+
// BLENDER: extra migration for backport, without bumping version.
655+
// Remove when upgrading to Gitea 1.22.
656+
type AuthToken struct {
657+
ID string `xorm:"pk"`
658+
TokenHash string
659+
UserID int64 `xorm:"INDEX"`
660+
ExpiresUnix timeutil.TimeStamp `xorm:"INDEX"`
661+
}
662+
if err = x.Sync(new(AuthToken)); err != nil {
663+
return fmt.Errorf("migration blender auth_token failed: %w", err)
664+
}
665+
652666
return nil
653667
}

modules/context/context_cookie.go

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,11 @@
44
package context
55

66
import (
7-
"encoding/hex"
87
"net/http"
98
"strings"
109

1110
"code.gitea.io/gitea/modules/setting"
12-
"code.gitea.io/gitea/modules/util"
1311
"code.gitea.io/gitea/modules/web/middleware"
14-
15-
"github.com/minio/sha256-simd"
16-
"golang.org/x/crypto/pbkdf2"
1712
)
1813

1914
const CookieNameFlash = "gitea_flash"
@@ -45,42 +40,3 @@ func (ctx *Context) DeleteSiteCookie(name string) {
4540
func (ctx *Context) GetSiteCookie(name string) string {
4641
return middleware.GetSiteCookie(ctx.Req, name)
4742
}
48-
49-
// GetSuperSecureCookie returns given cookie value from request header with secret string.
50-
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
51-
val := ctx.GetSiteCookie(name)
52-
return ctx.CookieDecrypt(secret, val)
53-
}
54-
55-
// CookieDecrypt returns given value from with secret string.
56-
func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {
57-
if val == "" {
58-
return "", false
59-
}
60-
61-
text, err := hex.DecodeString(val)
62-
if err != nil {
63-
return "", false
64-
}
65-
66-
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
67-
text, err = util.AESGCMDecrypt(key, text)
68-
return string(text), err == nil
69-
}
70-
71-
// SetSuperSecureCookie sets given cookie value to response header with secret string.
72-
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, maxAge int) {
73-
text := ctx.CookieEncrypt(secret, value)
74-
ctx.SetSiteCookie(name, text, maxAge)
75-
}
76-
77-
// CookieEncrypt encrypts a given value using the provided secret
78-
func (ctx *Context) CookieEncrypt(secret, value string) string {
79-
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
80-
text, err := util.AESGCMEncrypt(key, []byte(value))
81-
if err != nil {
82-
panic("error encrypting cookie: " + err.Error())
83-
}
84-
85-
return hex.EncodeToString(text)
86-
}

modules/setting/security.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ var (
1919
SecretKey string
2020
InternalToken string // internal access token
2121
LogInRememberDays int
22-
CookieUserName string
2322
CookieRememberName string
2423
ReverseProxyAuthUser string
2524
ReverseProxyAuthEmail string
@@ -104,7 +103,6 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
104103
sec := rootCfg.Section("security")
105104
InstallLock = HasInstallLock(rootCfg)
106105
LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
107-
CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome")
108106
SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
109107
if SecretKey == "" {
110108
// FIXME: https://github.com/go-gitea/gitea/issues/16832

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ disable_register_prompt = Registration is disabled. Please contact your site adm
358358
disable_register_mail = Email confirmation for registration is disabled.
359359
manual_activation_only = Contact your site administrator to complete activation.
360360
remember_me = Remember This Device
361+
remember_me.compromised = The login token is not valid anymore which may indicate a compromised account. Please check your account for unusual activities.
361362
forgot_password_title= Forgot Password
362363
forgot_password = Forgot password?
363364
sign_up_now = Need an account? Register now.

routers/install/install.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ import (
2727
"code.gitea.io/gitea/modules/log"
2828
"code.gitea.io/gitea/modules/setting"
2929
"code.gitea.io/gitea/modules/templates"
30+
"code.gitea.io/gitea/modules/timeutil"
3031
"code.gitea.io/gitea/modules/translation"
3132
"code.gitea.io/gitea/modules/user"
3233
"code.gitea.io/gitea/modules/util"
3334
"code.gitea.io/gitea/modules/web"
3435
"code.gitea.io/gitea/modules/web/middleware"
3536
"code.gitea.io/gitea/routers/common"
37+
auth_service "code.gitea.io/gitea/services/auth"
3638
"code.gitea.io/gitea/services/forms"
3739

3840
"gitea.com/go-chi/session"
@@ -547,11 +549,13 @@ func SubmitInstall(ctx *context.Context) {
547549
u, _ = user_model.GetUserByName(ctx, u.Name)
548550
}
549551

550-
days := 86400 * setting.LogInRememberDays
551-
ctx.SetSiteCookie(setting.CookieUserName, u.Name, days)
552+
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
553+
if err != nil {
554+
ctx.ServerError("CreateAuthTokenForUserID", err)
555+
return
556+
}
552557

553-
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
554-
setting.CookieRememberName, u.Name, days)
558+
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
555559

556560
// Auto-login for admin
557561
if err = ctx.Session.Set("uid", u.ID); err != nil {

routers/web/auth/2fa.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ var (
2626
func TwoFactor(ctx *context.Context) {
2727
ctx.Data["Title"] = ctx.Tr("twofa")
2828

29-
// Check auto-login.
30-
if checkAutoLogin(ctx) {
29+
if CheckAutoLogin(ctx) {
3130
return
3231
}
3332

@@ -99,8 +98,7 @@ func TwoFactorPost(ctx *context.Context) {
9998
func TwoFactorScratch(ctx *context.Context) {
10099
ctx.Data["Title"] = ctx.Tr("twofa_scratch")
101100

102-
// Check auto-login.
103-
if checkAutoLogin(ctx) {
101+
if CheckAutoLogin(ctx) {
104102
return
105103
}
106104

routers/web/auth/auth.go

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,41 +43,52 @@ const (
4343
TplActivate base.TplName = "user/auth/activate"
4444
)
4545

46-
// AutoSignIn reads cookie and try to auto-login.
47-
func AutoSignIn(ctx *context.Context) (bool, error) {
46+
// autoSignIn reads cookie and try to auto-login.
47+
func autoSignIn(ctx *context.Context) (bool, error) {
4848
if !db.HasEngine {
4949
return false, nil
5050
}
5151

52-
uname := ctx.GetSiteCookie(setting.CookieUserName)
53-
if len(uname) == 0 {
54-
return false, nil
55-
}
56-
5752
isSucceed := false
5853
defer func() {
5954
if !isSucceed {
60-
log.Trace("auto-login cookie cleared: %s", uname)
61-
ctx.DeleteSiteCookie(setting.CookieUserName)
6255
ctx.DeleteSiteCookie(setting.CookieRememberName)
6356
}
6457
}()
6558

66-
u, err := user_model.GetUserByName(ctx, uname)
59+
if err := auth.DeleteExpiredAuthTokens(ctx); err != nil {
60+
log.Error("Failed to delete expired auth tokens: %v", err)
61+
}
62+
63+
t, err := auth_service.CheckAuthToken(ctx, ctx.GetSiteCookie(setting.CookieRememberName))
6764
if err != nil {
68-
if !user_model.IsErrUserNotExist(err) {
69-
return false, fmt.Errorf("GetUserByName: %w", err)
65+
switch err {
66+
case auth_service.ErrAuthTokenInvalidFormat, auth_service.ErrAuthTokenExpired:
67+
return false, nil
7068
}
69+
return false, err
70+
}
71+
if t == nil {
7172
return false, nil
7273
}
7374

74-
if val, ok := ctx.GetSuperSecureCookie(
75-
base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); !ok || val != u.Name {
75+
u, err := user_model.GetUserByID(ctx, t.UserID)
76+
if err != nil {
77+
if !user_model.IsErrUserNotExist(err) {
78+
return false, fmt.Errorf("GetUserByID: %w", err)
79+
}
7680
return false, nil
7781
}
7882

7983
isSucceed = true
8084

85+
nt, token, err := auth_service.RegenerateAuthToken(ctx, t)
86+
if err != nil {
87+
return false, err
88+
}
89+
90+
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
91+
8192
if err := updateSession(ctx, nil, map[string]any{
8293
// Set session IDs
8394
"uid": u.ID,
@@ -113,11 +124,15 @@ func resetLocale(ctx *context.Context, u *user_model.User) error {
113124
return nil
114125
}
115126

116-
func checkAutoLogin(ctx *context.Context) bool {
127+
func CheckAutoLogin(ctx *context.Context) bool {
117128
// Check auto-login
118-
isSucceed, err := AutoSignIn(ctx)
129+
isSucceed, err := autoSignIn(ctx)
119130
if err != nil {
120-
ctx.ServerError("AutoSignIn", err)
131+
if errors.Is(err, auth_service.ErrAuthTokenInvalidHash) {
132+
ctx.Flash.Error(ctx.Tr("auth.remember_me.compromised"), true)
133+
return false
134+
}
135+
ctx.ServerError("autoSignIn", err)
121136
return true
122137
}
123138

@@ -161,8 +176,7 @@ func checkForceOAuth(ctx *context.Context) bool {
161176
func SignIn(ctx *context.Context) {
162177
ctx.Data["Title"] = ctx.Tr("sign_in")
163178

164-
// Check auto-login
165-
if checkAutoLogin(ctx) {
179+
if CheckAutoLogin(ctx) {
166180
return
167181
}
168182

@@ -315,10 +329,13 @@ func handleSignIn(ctx *context.Context, u *user_model.User, remember bool) {
315329

316330
func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRedirect bool) string {
317331
if remember {
318-
days := 86400 * setting.LogInRememberDays
319-
ctx.SetSiteCookie(setting.CookieUserName, u.Name, days)
320-
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
321-
setting.CookieRememberName, u.Name, days)
332+
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
333+
if err != nil {
334+
ctx.ServerError("CreateAuthTokenForUserID", err)
335+
return setting.AppSubURL + "/"
336+
}
337+
338+
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
322339
}
323340

324341
if err := updateSession(ctx, []string{
@@ -393,7 +410,6 @@ func getUserName(gothUser *goth.User) string {
393410
func HandleSignOut(ctx *context.Context) {
394411
_ = ctx.Session.Flush()
395412
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
396-
ctx.DeleteSiteCookie(setting.CookieUserName)
397413
ctx.DeleteSiteCookie(setting.CookieRememberName)
398414
ctx.Csrf.DeleteCookie(ctx)
399415
middleware.DeleteRedirectToCookie(ctx.Resp)

routers/web/auth/openid.go

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"code.gitea.io/gitea/modules/setting"
1717
"code.gitea.io/gitea/modules/util"
1818
"code.gitea.io/gitea/modules/web"
19-
"code.gitea.io/gitea/modules/web/middleware"
2019
"code.gitea.io/gitea/services/auth"
2120
"code.gitea.io/gitea/services/forms"
2221
)
@@ -36,23 +35,7 @@ func SignInOpenID(ctx *context.Context) {
3635
return
3736
}
3837

39-
// Check auto-login.
40-
isSucceed, err := AutoSignIn(ctx)
41-
if err != nil {
42-
ctx.ServerError("AutoSignIn", err)
43-
return
44-
}
45-
46-
redirectTo := ctx.FormString("redirect_to")
47-
if len(redirectTo) > 0 {
48-
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
49-
} else {
50-
redirectTo = ctx.GetSiteCookie("redirect_to")
51-
}
52-
53-
if isSucceed {
54-
middleware.DeleteRedirectToCookie(ctx.Resp)
55-
ctx.RedirectToFirst(redirectTo)
38+
if CheckAutoLogin(ctx) {
5639
return
5740
}
5841

0 commit comments

Comments
 (0)