fixed verificatoin
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/db/verification"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func normalizedIdempotencyKey(value *string) (string, bool) {
|
||||
@@ -24,6 +25,10 @@ func normalizedIdempotencyKey(value *string) (string, bool) {
|
||||
return key, true
|
||||
}
|
||||
|
||||
func syntheticIdempotencyKey() string {
|
||||
return "auto:" + bson.NewObjectID().Hex()
|
||||
}
|
||||
|
||||
func idempotencyFilter(
|
||||
request *verification.Request,
|
||||
idempotencyKey string,
|
||||
@@ -119,6 +124,13 @@ func (db *verificationDB) Create(
|
||||
}
|
||||
|
||||
idempotencyKey, hasIdempotency := normalizedIdempotencyKey(request.IdempotencyKey)
|
||||
if !hasIdempotency {
|
||||
// Legacy deployments may still enforce uniqueness on (accountRef, purpose, target, idempotencyKey),
|
||||
// where missing idempotency key behaves like a shared null key. Assign an internal per-request key
|
||||
// so token reissue works even when callers do not provide idempotency explicitly.
|
||||
idempotencyKey = syntheticIdempotencyKey()
|
||||
hasIdempotency = true
|
||||
}
|
||||
|
||||
token, raw, err := newVerificationToken(request, idempotencyKey, hasIdempotency)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package verificationimp
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -101,6 +102,27 @@ func (m *memoryTokenRepository) Insert(_ context.Context, obj storable.Storable,
|
||||
if _, exists := m.data[*id]; exists {
|
||||
return merrors.DataConflict("token already exists")
|
||||
}
|
||||
for _, existing := range m.data {
|
||||
if existing.VerifyTokenHash == tok.VerifyTokenHash {
|
||||
return merrors.DataConflict("duplicate verifyTokenHash")
|
||||
}
|
||||
if existing.AccountRef != tok.AccountRef {
|
||||
continue
|
||||
}
|
||||
if existing.Purpose != tok.Purpose {
|
||||
continue
|
||||
}
|
||||
if existing.Target != tok.Target {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case existing.IdempotencyKey == nil && tok.IdempotencyKey == nil:
|
||||
return merrors.DataConflict("duplicate verification context idempotency")
|
||||
case existing.IdempotencyKey != nil && tok.IdempotencyKey != nil && *existing.IdempotencyKey == *tok.IdempotencyKey:
|
||||
return merrors.DataConflict("duplicate verification context idempotency")
|
||||
}
|
||||
}
|
||||
m.data[*id] = cloneToken(tok)
|
||||
m.order = append(m.order, *id)
|
||||
return nil
|
||||
@@ -633,6 +655,49 @@ func TestCreate_InvalidatesPreviousToken(t *testing.T) {
|
||||
assert.Equal(t, accountRef, tok.AccountRef)
|
||||
}
|
||||
|
||||
func TestCreate_AccountActivationResendWithoutIdempotency_ReissuesToken(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
// First issue during signup.
|
||||
firstRaw, err := db.Create(ctx, req(accountRef, model.PurposeAccountActivation, "", time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second issue during resend should rotate token instead of failing with duplicate key.
|
||||
secondRaw, err := db.Create(ctx, req(accountRef, model.PurposeAccountActivation, "", time.Hour))
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, firstRaw, secondRaw)
|
||||
|
||||
// Old token becomes unusable after reissue.
|
||||
_, err = db.Consume(ctx, bson.NilObjectID, model.PurposeAccountActivation, firstRaw)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed))
|
||||
|
||||
// New token is valid.
|
||||
tok, err := db.Consume(ctx, bson.NilObjectID, model.PurposeAccountActivation, secondRaw)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, accountRef, tok.AccountRef)
|
||||
assert.Equal(t, model.PurposeAccountActivation, tok.Purpose)
|
||||
|
||||
// Non-idempotent requests should still persist unique internal keys,
|
||||
// preventing uniqueness collisions on (accountRef, purpose, target, idempotencyKey).
|
||||
repo := db.Repository.(*memoryTokenRepository)
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
keys := map[string]struct{}{}
|
||||
for _, stored := range repo.data {
|
||||
if stored.AccountRef != accountRef || stored.Purpose != model.PurposeAccountActivation {
|
||||
continue
|
||||
}
|
||||
require.NotNil(t, stored.IdempotencyKey)
|
||||
assert.True(t, strings.HasPrefix(*stored.IdempotencyKey, "auto:"))
|
||||
keys[*stored.IdempotencyKey] = struct{}{}
|
||||
}
|
||||
assert.Len(t, keys, 2)
|
||||
}
|
||||
|
||||
func TestCreate_InvalidatesMultiplePreviousTokens(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user