mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-15 08:15:36 +02:00
The web 2FA login and password-reset paths validated the passcode and then wrote LastUsedPasscode in a non-atomic read-check-write sequence, so two parallel submissions of the same code could each authenticate (TOCTOU). The Basic-Auth X-Gitea-OTP path never recorded the used passcode at all, letting a captured code be replayed for its whole validity window. Add TwoFactor.ValidateAndConsumeTOTP, which validates and atomically marks the passcode used via a conditional UPDATE (rejecting replays and racing duplicates), and route the web login, password-reset, and Basic-Auth paths through it. Assisted-by: Claude:claude-opus-4-8
48 lines
1.4 KiB
Go
48 lines
1.4 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
auth_model "gitea.dev/models/auth"
|
|
"gitea.dev/models/unittest"
|
|
|
|
"github.com/pquerna/otp/totp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestTwoFactorValidateAndConsumeTOTP(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
key, err := totp.Generate(totp.GenerateOpts{SecretSize: 40, Issuer: "gitea-test", AccountName: "consume"})
|
|
require.NoError(t, err)
|
|
|
|
tfa := &auth_model.TwoFactor{UID: 1}
|
|
require.NoError(t, tfa.SetSecret(key.Secret()))
|
|
require.NoError(t, auth_model.NewTwoFactor(t.Context(), tfa))
|
|
|
|
passcode, err := totp.GenerateCode(key.Secret(), time.Now())
|
|
require.NoError(t, err)
|
|
|
|
// first use of a valid passcode succeeds
|
|
ok, err := tfa.ValidateAndConsumeTOTP(t.Context(), passcode)
|
|
require.NoError(t, err)
|
|
assert.True(t, ok)
|
|
|
|
// replaying the same passcode is refused, even when still inside the TOTP validity window
|
|
reloaded, err := auth_model.GetTwoFactorByUID(t.Context(), tfa.UID)
|
|
require.NoError(t, err)
|
|
ok, err = reloaded.ValidateAndConsumeTOTP(t.Context(), passcode)
|
|
require.NoError(t, err)
|
|
assert.False(t, ok)
|
|
|
|
// an invalid passcode is rejected without consuming anything
|
|
ok, err = reloaded.ValidateAndConsumeTOTP(t.Context(), "000000")
|
|
require.NoError(t, err)
|
|
assert.False(t, ok)
|
|
}
|