Skip to content

Commit c6c829f

Browse files
authored
Enhanced auth token / remember me (#27606)
Closes #27455 > 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 ee6a390 commit c6c829f

23 files changed

+418
-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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,8 @@ var migrations = []Migration{
546546

547547
// v280 -> v281
548548
NewMigration("Rename user themes", v1_22.RenameUserThemes),
549+
// v281 -> v282
550+
NewMigration("Add auth_token table", v1_22.CreateAuthTokenTable),
549551
}
550552

551553
// GetCurrentDBVersion returns the current db version

models/migrations/v1_22/v281.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_22 //nolint
5+
6+
import (
7+
"code.gitea.io/gitea/modules/timeutil"
8+
9+
"xorm.io/xorm"
10+
)
11+
12+
func CreateAuthTokenTable(x *xorm.Engine) error {
13+
type AuthToken struct {
14+
ID string `xorm:"pk"`
15+
TokenHash string
16+
UserID int64 `xorm:"INDEX"`
17+
ExpiresUnix timeutil.TimeStamp `xorm:"INDEX"`
18+
}
19+
20+
return x.Sync(new(AuthToken))
21+
}

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
@@ -363,6 +363,7 @@ disable_register_prompt = Registration is disabled. Please contact your site adm
363363
disable_register_mail = Email confirmation for registration is disabled.
364364
manual_activation_only = Contact your site administrator to complete activation.
365365
remember_me = Remember This Device
366+
remember_me.compromised = The login token is not valid anymore which may indicate a compromised account. Please check your account for unusual activities.
366367
forgot_password_title= Forgot Password
367368
forgot_password = Forgot password?
368369
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

@@ -141,8 +156,7 @@ func checkAutoLogin(ctx *context.Context) bool {
141156
func SignIn(ctx *context.Context) {
142157
ctx.Data["Title"] = ctx.Tr("sign_in")
143158

144-
// Check auto-login
145-
if checkAutoLogin(ctx) {
159+
if CheckAutoLogin(ctx) {
146160
return
147161
}
148162

@@ -290,10 +304,13 @@ func handleSignIn(ctx *context.Context, u *user_model.User, remember bool) {
290304

291305
func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRedirect bool) string {
292306
if remember {
293-
days := 86400 * setting.LogInRememberDays
294-
ctx.SetSiteCookie(setting.CookieUserName, u.Name, days)
295-
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
296-
setting.CookieRememberName, u.Name, days)
307+
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
308+
if err != nil {
309+
ctx.ServerError("CreateAuthTokenForUserID", err)
310+
return setting.AppSubURL + "/"
311+
}
312+
313+
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
297314
}
298315

299316
if err := updateSession(ctx, []string{
@@ -368,7 +385,6 @@ func getUserName(gothUser *goth.User) string {
368385
func HandleSignOut(ctx *context.Context) {
369386
_ = ctx.Session.Flush()
370387
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
371-
ctx.DeleteSiteCookie(setting.CookieUserName)
372388
ctx.DeleteSiteCookie(setting.CookieRememberName)
373389
ctx.Csrf.DeleteCookie(ctx)
374390
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)