fix(auth): avoid "compromised account" false-alarm when login with remember-me token (#39686)

1. Remember the last token that has been rotated and skip its validation
2. Don't alarm when the token mismatch occurs in a short window
This commit is contained in:
wxiaoguang authored and GitHub committed 2026-10-12 08:44:52 +00:00
1 parent 8c7b8d085c
commit 2f1761e3fe
7 files changed
+118 -36

No files matched your search

+30 -22
View File
@@ -78,49 +78,54 @@ func prepareCommonAuthPageData(ctx *context.Context, opt CommonAuthOptions) {
// autoSignIn reads cookie and try to auto-login.
func autoSignIn(ctx *context.Context) (bool, error) {
isSucceed := false
defer func() {
if !isSucceed {
ctx.DeleteSiteCookie(setting.CookieRememberName)
}
}()
cookieTokenValue := ctx.GetSiteCookie(setting.CookieRememberName)
if cookieTokenValue == "" {
return false, nil
}
if err := auth.DeleteExpiredAuthTokens(ctx); err != nil {
log.Error("Failed to delete expired auth tokens: %v", err)
}
t, err := auth_service.CheckAuthToken(ctx, ctx.GetSiteCookie(setting.CookieRememberName))
if err != nil {
switch err {
case auth_service.ErrAuthTokenInvalidFormat, auth_service.ErrAuthTokenExpired:
return false, nil
deleteCookie := true
defer func() {
if deleteCookie {
ctx.DeleteSiteCookie(setting.CookieRememberName)
}
return false, err
}
if t == nil {
}()
t, err := auth_service.CheckAuthToken(ctx, cookieTokenValue)
if errors.Is(err, auth_service.ErrAuthTokenStale) {
deleteCookie = false
return false, nil
} else if errors.Is(err, auth_service.ErrAuthTokenInvalidFormat) || errors.Is(err, auth_service.ErrAuthTokenExpired) {
return false, nil
} else if err != nil {
return false, err
}
u, err := user_model.GetUserByID(ctx, t.UserID)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
return false, fmt.Errorf("GetUserByID: %w", err)
}
if user_model.IsErrUserNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("GetUserByID: %w", err)
}
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
if err != nil {
return false, fmt.Errorf("HasTwoFactorOrWebAuthn: %w", err)
}
isSucceed = true
nt, token, err := auth_service.RegenerateAuthToken(ctx, t)
if err != nil {
if errors.Is(err, auth_service.ErrAuthTokenStale) {
deleteCookie = false
return false, nil
} else if err != nil {
return false, err
}
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
deleteCookie = false
if err := regenerateSession(ctx, map[string]any{
session.KeyUID: u.ID,
@@ -130,7 +135,7 @@ func autoSignIn(ctx *context.Context) (bool, error) {
}
if err := resetLocale(ctx, u); err != nil {
return false, err
log.Error("Failed to reset locale for user %d: %v", u.ID, err)
}
return true, nil
@@ -374,6 +379,9 @@ func handleSignIn(ctx *context.Context, u *user_model.User, remember bool) {
func handleSignInFull(ctx *context.Context, u *user_model.User, remember bool) {
if remember {
if err := auth.DeleteExpiredAuthTokens(ctx); err != nil {
log.Error("Failed to delete expired auth tokens: %v", err)
}
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
if err != nil {
ctx.ServerError("CreateAuthTokenForUserID", err)
+20
View File
@@ -18,6 +18,7 @@ import (
"gitea.dev/modules/test"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
auth_service "gitea.dev/services/auth"
"gitea.dev/services/auth/source/oauth2"
"gitea.dev/services/contexttest"
"gitea.dev/services/forms"
@@ -216,3 +217,22 @@ func TestRegisterOpenIDPostRejectsWrongCaptcha(t *testing.T) {
assert.Equal(t, true, ctx.Data["Err_Captcha"])
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "openid-captcha-user"})
}
func TestAutoSignInStaleCookie(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
at, token, err := auth_service.CreateAuthTokenForUserID(t.Context(), 2)
require.NoError(t, err)
current, currentToken, err := auth_service.RegenerateAuthToken(t.Context(), at)
require.NoError(t, err)
ctx, resp := contexttest.MockContext(t, "/user/login")
ctx.Req.AddCookie(&http.Cookie{Name: setting.CookieRememberName, Value: at.ID + ":" + token})
succeeded, err := autoSignIn(ctx)
require.NoError(t, err)
require.False(t, succeeded)
for _, cookie := range resp.Result().Cookies() {
assert.NotEqual(t, setting.CookieRememberName, cookie.Name)
}
_, err = auth_service.CheckAuthToken(t.Context(), current.ID+":"+currentToken)
require.NoError(t, err)
}