feat(captcha): in-process nonce store for single-use enforcement
This commit is contained in:
parent
f5cbb56df7
commit
7b15ce70d0
39
captcha/nonce.go
Normal file
39
captcha/nonce.go
Normal file
@ -0,0 +1,39 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NonceStore tracks used challenge nonces for single-use enforcement. The
|
||||
// default MemoryNonceStore is per-process (no DB) — sufficient for the 5a
|
||||
// model. A durable cross-process store (5b) can implement this interface later.
|
||||
type NonceStore interface {
|
||||
// MarkUsed records nonce; returns true on first use, false if already seen.
|
||||
MarkUsed(nonce string, ttl time.Duration) bool
|
||||
}
|
||||
|
||||
type MemoryNonceStore struct {
|
||||
mu sync.Mutex
|
||||
used map[string]time.Time
|
||||
}
|
||||
|
||||
func NewMemoryNonceStore() *MemoryNonceStore {
|
||||
return &MemoryNonceStore{used: make(map[string]time.Time)}
|
||||
}
|
||||
|
||||
func (m *MemoryNonceStore) MarkUsed(nonce string, ttl time.Duration) bool {
|
||||
now := time.Now()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for k, exp := range m.used {
|
||||
if !exp.After(now) {
|
||||
delete(m.used, k)
|
||||
}
|
||||
}
|
||||
if _, seen := m.used[nonce]; seen {
|
||||
return false
|
||||
}
|
||||
m.used[nonce] = now.Add(ttl)
|
||||
return true
|
||||
}
|
||||
27
captcha/nonce_test.go
Normal file
27
captcha/nonce_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMemoryNonceStoreSingleUse(t *testing.T) {
|
||||
st := NewMemoryNonceStore()
|
||||
if !st.MarkUsed("n1", time.Minute) {
|
||||
t.Fatal("first use should return true")
|
||||
}
|
||||
if st.MarkUsed("n1", time.Minute) {
|
||||
t.Fatal("second use should return false")
|
||||
}
|
||||
if !st.MarkUsed("n2", time.Minute) {
|
||||
t.Fatal("distinct nonce should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryNonceStoreExpiry(t *testing.T) {
|
||||
st := NewMemoryNonceStore()
|
||||
st.MarkUsed("n1", -time.Second) // already expired
|
||||
if !st.MarkUsed("n1", time.Minute) {
|
||||
t.Fatal("expired nonce should be reusable")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user