fix(auth): prevent competing remember-me token rotations (#39686) (#39688)

Partially backport #39686
This commit is contained in:
wxiaoguang authored and GitHub committed 2026-10-12 09:48:37 +00:00
1 parent 6064554b8d
commit 33aaf0cfa3
4 files changed
+54 -11

No files matched your search

+19 -4
View File
@@ -24,9 +24,10 @@ import (
// If a device uses the token to login into the instance, a fresh token gets generated which has the same id but a new hash.
var (
ErrAuthTokenInvalidFormat = util.NewInvalidArgumentErrorf("auth token has an invalid format")
ErrAuthTokenExpired = util.NewInvalidArgumentErrorf("auth token has expired")
ErrAuthTokenInvalidHash = util.NewInvalidArgumentErrorf("auth token is invalid")
ErrAuthTokenInvalidFormat = util.NewInvalidArgumentErrorf("auth token has an invalid format")
ErrAuthTokenExpired = util.NewInvalidArgumentErrorf("auth token has expired")
ErrAuthTokenInvalidHash = util.NewInvalidArgumentErrorf("auth token is invalid")
ErrAuthTokenRotationConflict = util.NewInvalidArgumentErrorf("auth token was rotated by another request")
)
func CheckAuthToken(ctx context.Context, value string) (*auth_model.AuthToken, error) {
@@ -76,9 +77,23 @@ func RegenerateAuthToken(ctx context.Context, t *auth_model.AuthToken) (*auth_mo
ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour),
}
if err := auth_model.UpdateAuthTokenByID(ctx, newToken); err != nil {
updated, err := auth_model.UpdateAuthTokenByID(ctx, newToken, t.TokenHash)
if err != nil {
return nil, "", err
}
if !updated {
current, err := auth_model.GetAuthTokenByID(ctx, t.ID)
if errors.Is(err, util.ErrNotExist) {
return nil, "", ErrAuthTokenExpired
}
if err != nil {
return nil, "", err
}
if current.ExpiresUnix < timeutil.TimeStampNow() {
return nil, "", ErrAuthTokenExpired
}
return nil, "", ErrAuthTokenRotationConflict
}
return newToken, token, nil
}
+21
View File
@@ -13,6 +13,7 @@ import (
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCheckAuthToken(t *testing.T) {
@@ -105,5 +106,25 @@ func TestRegenerateAuthToken(t *testing.T) {
assert.NotEqual(t, token, token2)
assert.NotEqual(t, at.ExpiresUnix, at2.ExpiresUnix)
// Only one request may rotate the same validated cookie.
competing, competingToken, err := RegenerateAuthToken(t.Context(), at)
require.ErrorIs(t, err, ErrAuthTokenRotationConflict)
require.Nil(t, competing)
require.Empty(t, competingToken)
current, err := CheckAuthToken(t.Context(), at2.ID+":"+token2)
require.NoError(t, err)
require.Equal(t, at2.TokenHash, current.TokenHash)
assert.NoError(t, auth_model.DeleteAuthTokenByID(t.Context(), at.ID))
}
func TestRegenerateRevokedAuthToken(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
at, _, err := CreateAuthTokenForUserID(t.Context(), 2)
require.NoError(t, err)
require.NoError(t, auth_model.DeleteAuthTokenByID(t.Context(), at.ID))
rotated, token, err := RegenerateAuthToken(t.Context(), at)
require.ErrorIs(t, err, ErrAuthTokenExpired)
require.Nil(t, rotated)
require.Empty(t, token)
}