feat: update gitea vendor
runner nix smoke / nix label and flake smoke (push) Failing after 1m28s

This commit is contained in:
2026-09-26 21:18:24 +00:00
parent c439c1b948
commit d9b2a4e787
3538 changed files with 116131 additions and 44340 deletions
@@ -11,51 +11,15 @@ import (
"fmt"
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"gitea.dev/models/db"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
lru "github.com/hashicorp/golang-lru/v2"
"xorm.io/builder"
)
// ErrAccessTokenNotExist represents a "AccessTokenNotExist" kind of error.
type ErrAccessTokenNotExist struct {
Token string
}
// IsErrAccessTokenNotExist checks if an error is a ErrAccessTokenNotExist.
func IsErrAccessTokenNotExist(err error) bool {
_, ok := err.(ErrAccessTokenNotExist)
return ok
}
func (err ErrAccessTokenNotExist) Error() string {
return fmt.Sprintf("access token does not exist [sha: %s]", err.Token)
}
func (err ErrAccessTokenNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrAccessTokenEmpty represents a "AccessTokenEmpty" kind of error.
type ErrAccessTokenEmpty struct{}
// IsErrAccessTokenEmpty checks if an error is a ErrAccessTokenEmpty.
func IsErrAccessTokenEmpty(err error) bool {
_, ok := err.(ErrAccessTokenEmpty)
return ok
}
func (err ErrAccessTokenEmpty) Error() string {
return "access token is empty"
}
func (err ErrAccessTokenEmpty) Unwrap() error {
return util.ErrInvalidArgument
}
var successfulAccessTokenCache *lru.Cache[string, any]
// AccessToken represents a personal access token.
@@ -98,19 +62,13 @@ func init() {
// NewAccessToken creates new access token.
func NewAccessToken(ctx context.Context, t *AccessToken) error {
salt, err := util.CryptoRandomString(10)
if err != nil {
return err
}
token, err := util.CryptoRandomBytes(20)
if err != nil {
return err
}
salt := util.CryptoRandomString(10)
token := util.CryptoRandomBytes(20)
t.TokenSalt = salt
t.Token = hex.EncodeToString(token)
t.TokenHash = HashToken(t.Token, t.TokenSalt)
t.TokenLastEight = t.Token[len(t.Token)-8:]
_, err = db.GetEngine(ctx).Insert(t)
_, err := db.GetEngine(ctx).Insert(t)
return err
}
@@ -140,21 +98,11 @@ func getAccessTokenIDFromCache(token string) int64 {
// GetAccessTokenBySHA returns access token by given token value
func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error) {
if token == "" {
return nil, ErrAccessTokenEmpty{}
}
// A token is defined as being SHA1 sum these are 40 hexadecimal bytes long
if len(token) != 40 {
return nil, ErrAccessTokenNotExist{token}
}
for _, x := range []byte(token) {
if x < '0' || (x > '9' && x < 'a') || x > 'f' {
return nil, ErrAccessTokenNotExist{token}
}
if len(token) < 8 {
return nil, util.NewNotExistErrorf("access token not found")
}
lastEight := token[len(token)-8:]
if id := getAccessTokenIDFromCache(token); id > 0 {
accessToken := &AccessToken{
TokenLastEight: lastEight,
@@ -175,7 +123,7 @@ func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error
if err != nil {
return nil, err
} else if len(tokens) == 0 {
return nil, ErrAccessTokenNotExist{token}
return nil, util.NewNotExistErrorf("access token not found")
}
for _, t := range tokens {
@@ -187,7 +135,7 @@ func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error
return &t, nil
}
}
return nil, ErrAccessTokenNotExist{token}
return nil, util.NewNotExistErrorf("access token not found")
}
// AccessTokenByNameExists checks if a token name has been used already by a user.
@@ -224,13 +172,11 @@ func UpdateAccessToken(ctx context.Context, t *AccessToken) error {
// DeleteAccessTokenByID deletes access token by given ID.
func DeleteAccessTokenByID(ctx context.Context, id, userID int64) error {
cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{
UID: userID,
})
cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{UID: userID})
if err != nil {
return err
} else if cnt != 1 {
return ErrAccessTokenNotExist{}
return util.NewNotExistErrorf("access token not found")
}
return nil
}
@@ -8,7 +8,7 @@ import (
"slices"
"strings"
"code.gitea.io/gitea/models/perm"
"gitea.dev/models/perm"
)
// AccessTokenScopeCategory represents the scope category for an access token
@@ -304,6 +304,36 @@ func (s AccessTokenScope) PublicOnly() (bool, error) {
return bitmap.hasScope(AccessTokenScopePublicOnly)
}
// CanCreateChildScope reports whether a request authenticated by this (parent) scope may mint a token
// carrying the child scope. It rejects any grantable scope the parent does not hold, closing the
// scope-escalation path. public-only is a restriction rather than a grantable permission, so it is
// ignored here (a child may always be public-only); EnforcePublicOnlyFrom handles carrying it down.
func (s AccessTokenScope) CanCreateChildScope(child AccessTokenScope) (bool, error) {
requested := child.StringSlice()
scopes := make([]AccessTokenScope, 0, len(requested))
for _, sc := range requested {
childScope := AccessTokenScope(sc)
if childScope == AccessTokenScopePublicOnly {
continue
}
scopes = append(scopes, childScope)
}
return s.HasScope(scopes...)
}
// EnforcePublicOnlyFrom adds the public-only restriction to s when the authorizing parent scope is
// public-only, so a public-only token cannot mint a child token that drops the restriction.
func (s AccessTokenScope) EnforcePublicOnlyFrom(parent AccessTokenScope) (AccessTokenScope, error) {
publicOnly, err := parent.PublicOnly()
if err != nil {
return "", err
}
if !publicOnly {
return s, nil
}
return AccessTokenScope(string(s) + "," + string(AccessTokenScopePublicOnly)).Normalize()
}
// HasScope returns true if the string has the given scope
func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) {
bitmap, err := s.parse()
@@ -89,3 +89,26 @@ func TestAccessTokenScope_HasScope(t *testing.T) {
})
}
}
func TestAccessTokenScope_EnforcePublicOnlyFrom(t *testing.T) {
tests := []struct {
in AccessTokenScope
parent AccessTokenScope
out AccessTokenScope
}{
// public-only parent forces the restriction onto the minted scope
{"write:user", "write:user,public-only", "public-only,write:user"},
// already public-only stays public-only
{"public-only,read:user", "public-only", "public-only,read:user"},
// non-public-only parent leaves the scope untouched
{"write:user", "write:user", "write:user"},
{"all", "all", "all"},
}
for _, test := range tests {
t.Run(string(test.parent)+"->"+string(test.in), func(t *testing.T) {
got, err := test.in.EnforcePublicOnlyFrom(test.parent)
assert.NoError(t, err)
assert.Equal(t, test.out, got)
})
}
}
@@ -6,9 +6,10 @@ package auth_test
import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
)
@@ -76,11 +77,11 @@ func TestGetAccessTokenBySHA(t *testing.T) {
_, err = auth_model.GetAccessTokenBySHA(t.Context(), "notahash")
assert.Error(t, err)
assert.True(t, auth_model.IsErrAccessTokenNotExist(err))
assert.ErrorIs(t, err, util.ErrNotExist)
_, err = auth_model.GetAccessTokenBySHA(t.Context(), "")
assert.Error(t, err)
assert.True(t, auth_model.IsErrAccessTokenEmpty(err))
assert.ErrorIs(t, err, util.ErrNotExist)
}
func TestListAccessTokens(t *testing.T) {
@@ -128,5 +129,5 @@ func TestDeleteAccessTokenByID(t *testing.T) {
err = auth_model.DeleteAccessTokenByID(t.Context(), 100, 100)
assert.Error(t, err)
assert.True(t, auth_model.IsErrAccessTokenNotExist(err))
assert.ErrorIs(t, err, util.ErrNotExist)
}
@@ -6,9 +6,9 @@ package auth
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
@@ -6,13 +6,13 @@ package auth_test
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"gitea.dev/models/unittest"
_ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
_ "code.gitea.io/gitea/models/auth"
_ "code.gitea.io/gitea/models/perm/access"
_ "gitea.dev/models"
_ "gitea.dev/models/actions"
_ "gitea.dev/models/activities"
_ "gitea.dev/models/auth"
_ "gitea.dev/models/perm/access"
)
func TestMain(m *testing.M) {
+14 -15
View File
@@ -15,11 +15,11 @@ import (
"strings"
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"gitea.dev/models/db"
"gitea.dev/modules/container"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
uuid "github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
@@ -83,6 +83,11 @@ func BuiltinApplications() map[string]*BuiltinOAuth2Application {
DisplayName: "tea",
RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"},
}
m["b757811a-05c8-4c76-8d74-a5ee3d2073f2"] = &BuiltinOAuth2Application{
ConfigName: "gitea-app",
DisplayName: "Gitea App",
RedirectURIs: []string{"com.gitea.app://oauth/callback"},
}
return m
}
@@ -198,10 +203,7 @@ var base32Lower = base32.NewEncoding(lowerBase32Chars).WithPadding(base32.NoPadd
// GenerateClientSecret will generate the client secret and returns the plaintext and saves the hash at the database
func (app *OAuth2Application) GenerateClientSecret(ctx context.Context) (string, error) {
rBytes, err := util.CryptoRandomBytes(32)
if err != nil {
return "", err
}
rBytes := util.CryptoRandomBytes(32)
// Add a prefix to the base32, this is in order to make it easier
// for code scanners to grab sensitive tokens.
clientSecret := "gto_" + base32Lower.EncodeToString(rBytes)
@@ -233,7 +235,7 @@ func (app *OAuth2Application) GetGrantByUserID(ctx context.Context, userID int64
return grant, nil
}
// CreateGrant generates a grant for an user
// CreateGrant generates a grant for a user
func (app *OAuth2Application) CreateGrant(ctx context.Context, userID int64, scope string) (*OAuth2Grant, error) {
grant := &OAuth2Grant{
ApplicationID: app.ID,
@@ -489,7 +491,7 @@ func GetOAuth2AuthorizationByCode(ctx context.Context, code string) (auth *OAuth
//////////////////////////////////////////////////////
// OAuth2Grant represents the permission of an user for a specific application to access resources
// OAuth2Grant represents the permission of a user for a specific application to access resources
type OAuth2Grant struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX unique(user_application)"`
@@ -509,10 +511,7 @@ func (grant *OAuth2Grant) TableName() string {
// GenerateNewAuthorizationCode generates a new authorization code for a grant and saves it to the database
func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redirectURI, codeChallenge, codeChallengeMethod string) (code *OAuth2AuthorizationCode, err error) {
rBytes, err := util.CryptoRandomBytes(32)
if err != nil {
return &OAuth2AuthorizationCode{}, err
}
rBytes := util.CryptoRandomBytes(32)
// Add a prefix to the base32, this is in order to make it easier
// for code scanners to grab sensitive tokens.
codeSecret := "gta_" + base32Lower.EncodeToString(rBytes)
@@ -4,7 +4,7 @@
package auth
import (
"code.gitea.io/gitea/models/db"
"gitea.dev/models/db"
"xorm.io/builder"
)
+25 -33
View File
@@ -7,25 +7,36 @@ import (
"testing"
"time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/timeutil"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/unittest"
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
)
func TestOAuth2AuthorizationCodeValidity(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
func TestOAuth2AuthorizationCode(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
t.Run("GenerateSetsValidUntil", func(t *testing.T) {
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600)
code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, expectedValidUntil, code.ValidUntil)
assert.False(t, code.IsExpired())
assert.Equal(t, int64(1), code.ID)
code2, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), code.Code)
require.NoError(t, err)
assert.Equal(t, code.Code, code2.Code)
assert.NoError(t, code.Invalidate(t.Context()))
code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist")
require.NoError(t, err)
require.Nil(t, code)
})
t.Run("Expired", func(t *testing.T) {
@@ -35,13 +46,14 @@ func TestOAuth2AuthorizationCodeValidity(t *testing.T) {
assert.True(t, code.IsExpired())
})
t.Run("InvalidateTwice", func(t *testing.T) {
code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode")
assert.NoError(t, err)
if assert.NotNil(t, code) {
assert.NoError(t, code.Invalidate(t.Context()))
assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated)
}
t.Run("Invalidate", func(t *testing.T) {
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "")
require.NoError(t, err)
require.NotNil(t, code)
require.NoError(t, code.Invalidate(t.Context()))
unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: code.Code})
assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated)
})
}
@@ -276,19 +288,6 @@ func TestRevokeOAuth2Grant(t *testing.T) {
//////////////////// Authorization Code
func TestGetOAuth2AuthorizationByCode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode")
assert.NoError(t, err)
assert.NotNil(t, code)
assert.Equal(t, "authcode", code.Code)
assert.Equal(t, int64(1), code.ID)
code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist")
assert.NoError(t, err)
assert.Nil(t, code)
}
func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) {
s256Verifier := "s256-verifier"
s256Challenge := oauth2.S256ChallengeFromVerifier(s256Verifier)
@@ -339,13 +338,6 @@ func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) {
assert.Equal(t, "https://example.com/callback?code=thecode", redirect.String())
}
func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
code := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"})
assert.NoError(t, code.Invalidate(t.Context()))
unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"})
}
func TestOAuth2AuthorizationCode_TableName(t *testing.T) {
assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName())
}
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
+7 -7
View File
@@ -9,12 +9,12 @@ import (
"fmt"
"reflect"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"gitea.dev/models/db"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
"xorm.io/xorm"
@@ -100,7 +100,7 @@ var registeredConfigs = map[Type]func() Config{}
// RegisterTypeConfig register a config for a provided type
func RegisterTypeConfig(typ Type, exemplar Config) {
if reflect.TypeOf(exemplar).Kind() == reflect.Ptr {
if reflect.TypeOf(exemplar).Kind() == reflect.Pointer {
// Pointer:
registeredConfigs[typ] = func() Config {
return reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(Config)
@@ -7,9 +7,9 @@ import (
"strings"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/json"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/unittest"
"gitea.dev/modules/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+35 -14
View File
@@ -13,14 +13,15 @@ import (
"encoding/hex"
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"gitea.dev/models/db"
"gitea.dev/modules/secret"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/pbkdf2"
"xorm.io/builder"
)
//
@@ -65,14 +66,11 @@ func init() {
// GenerateScratchToken recreates the scratch token the user is using.
func (t *TwoFactor) GenerateScratchToken() (string, error) {
tokenBytes, err := util.CryptoRandomBytes(6)
if err != nil {
return "", err
}
tokenBytes := util.CryptoRandomBytes(6)
// these chars are specially chosen, avoid ambiguous chars like `0`, `O`, `1`, `I`.
const base32Chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
token := base32.NewEncoding(base32Chars).WithPadding(base32.NoPadding).EncodeToString(tokenBytes)
t.ScratchSalt, _ = util.CryptoRandomString(10)
t.ScratchSalt = util.CryptoRandomString(10)
t.ScratchHash = HashToken(token, t.ScratchSalt)
return token, nil
}
@@ -107,20 +105,43 @@ func (t *TwoFactor) SetSecret(secretString string) error {
return nil
}
// ValidateTOTP validates the provided passcode.
func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
// validateTOTP validates the provided passcode. It does not consume the passcode; all login
// surfaces must go through ValidateAndConsumeTOTP so that a passcode cannot be redeemed twice.
func (t *TwoFactor) validateTOTP(passcode string) (bool, error) {
decodedStoredSecret, err := base64.StdEncoding.DecodeString(t.Secret)
if err != nil {
return false, fmt.Errorf("ValidateTOTP invalid base64: %w", err)
return false, fmt.Errorf("validateTOTP invalid base64: %w", err)
}
secretBytes, err := secret.AesDecrypt(t.getEncryptionKey(), decodedStoredSecret)
if err != nil {
return false, fmt.Errorf("ValidateTOTP unable to decrypt (maybe SECRET_KEY is wrong): %w", err)
return false, fmt.Errorf("validateTOTP unable to decrypt (maybe SECRET_KEY is wrong): %w", err)
}
secretStr := string(secretBytes)
return totp.Validate(passcode, secretStr), nil
}
// ValidateAndConsumeTOTP validates the passcode and atomically records it as used so that the
// same passcode cannot be redeemed more than once (RFC 6238 §5.2). It returns false for an
// invalid passcode as well as for a replay, including the case where a concurrent request with
// the same passcode won the race first. All TOTP login surfaces must go through this helper.
func (t *TwoFactor) ValidateAndConsumeTOTP(ctx context.Context, passcode string) (bool, error) {
ok, err := t.validateTOTP(passcode)
if err != nil || !ok {
return false, err
}
// Conditional update: only a row whose stored passcode differs from this one is updated, so a
// replay (or a concurrent duplicate) matches zero rows and is rejected. The row lock taken by
// the UPDATE serializes racing requests, closing the read-validate-write TOCTOU window.
t.LastUsedPasscode = passcode
n, err := db.GetEngine(ctx).ID(t.ID).
Where(builder.Or(builder.IsNull{"last_used_passcode"}, builder.Neq{"last_used_passcode": passcode})).
Cols("last_used_passcode").Update(t)
if err != nil {
return false, err
}
return n == 1, nil
}
// NewTwoFactor creates a new two-factor authentication token.
func NewTwoFactor(ctx context.Context, t *TwoFactor) error {
_, err := db.GetEngine(ctx).Insert(t)
@@ -0,0 +1,47 @@
// 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)
}
+3 -13
View File
@@ -8,9 +8,9 @@ import (
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
@@ -200,13 +200,3 @@ func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) {
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{})
return had > 0, err
}
// WebAuthnCredentials implements the webauthn.User interface
func WebAuthnCredentials(ctx context.Context, userID int64) ([]webauthn.Credential, error) {
dbCreds, err := GetWebAuthnCredentialsByUID(ctx, userID)
if err != nil {
return nil, err
}
return dbCreds.ToCredentials(), nil
}
@@ -6,8 +6,8 @@ package auth_test
import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/unittest"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/stretchr/testify/assert"