// 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/go-webauthn/webauthn/webauthn" "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) } func TestDisableTwoFactor(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) ctx := t.Context() const uid = 1000 // a uid with no user/2FA fixtures // Enroll TOTP and register a WebAuthn credential. tfa := &auth_model.TwoFactor{UID: uid} require.NoError(t, tfa.SetSecret("test-secret")) require.NoError(t, auth_model.NewTwoFactor(ctx, tfa)) _, err := auth_model.CreateCredential(ctx, uid, "test-key", &webauthn.Credential{ID: []byte("test-cred-id")}) require.NoError(t, err) has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, uid) require.NoError(t, err) require.True(t, has) // Both records are removed and counted separately. totp, webAuthn, err := auth_model.DisableTwoFactor(ctx, uid) require.NoError(t, err) assert.EqualValues(t, 1, totp) assert.EqualValues(t, 1, webAuthn) has, err = auth_model.HasTwoFactorOrWebAuthn(ctx, uid) require.NoError(t, err) assert.False(t, has) // A second call on a user without 2FA is a no-op. totp, webAuthn, err = auth_model.DisableTwoFactor(ctx, uid) require.NoError(t, err) assert.EqualValues(t, 0, totp) assert.EqualValues(t, 0, webAuthn) }