replaced in memory nonce in redis - #361
Open
KAMALDEEN333 wants to merge 3 commits into
Open
Conversation
| nonce = _generate_nonce(wallet_id) | ||
| assert _consume_nonce(nonce, wallet_id) is True | ||
| nonce = await _generate_nonce(wallet_id) | ||
| assert await _consume_nonce(nonce, wallet_id) is True |
| nonce = await _generate_nonce(wallet_id) | ||
| await _consume_nonce(nonce, wallet_id) | ||
| stored_wallet = await _mock_redis.get(_nonce_key(nonce)) | ||
| assert stored_wallet is None |
| assert _consume_nonce(nonce, wallet_id) is True | ||
| assert _consume_nonce(nonce, wallet_id) is False | ||
| nonce = await _generate_nonce(wallet_id) | ||
| assert await _consume_nonce(nonce, wallet_id) is True |
| assert _consume_nonce(nonce, wallet_id) is False | ||
| nonce = await _generate_nonce(wallet_id) | ||
| assert await _consume_nonce(nonce, wallet_id) is True | ||
| assert await _consume_nonce(nonce, wallet_id) is False |
| assert _consume_nonce(nonce, "GATTACKER") is False | ||
| async def test_wrong_wallet_id_returns_false(self): | ||
| nonce = await _generate_nonce("GOWNER") | ||
| assert await _consume_nonce(nonce, "GATTACKER") is False |
| _generate_nonce("GNEW") | ||
| assert stale_nonce not in _nonce_store | ||
| async def test_unknown_nonce_returns_false(self): | ||
| assert await _consume_nonce("deadbeef" * 8, "GABCDEF") is False |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replace in-memory nonce store with Redis
closes #192
Contributor
Problem Statement
The signed-nonce authentication in wallet_auth.py keeps state in a
module-level dictionary:
_nonce_store: Dict[str, Tuple[str, float]] = {}
def clean_expired_nonces():
cutoff = time.monotonic() - NONCE_TTL
expired = [n for n, (, ts) in _nonce_store.items() if ts < cutoff]
for n in expired: _nonce_store.pop(n, None)
This dict is a per-process singleton. Two uvicorn workers do not share
state, so a nonce can be issued in worker A and consumed successfully in
worker B but rejected if nonce was issued in B and verified in A. Worse:
after a uvicorn reload, the dict is blown away, so issued-but-unused
nonces vanish (denial-of-service-by-deploy) and issued-and-consumed
nonce metadata is gone (ban replay-protection).
Why It Matters
This is the foundational replay-protection for the protocol. Without
shared state, the auth model collapses the moment the deployment hits >1
uvicorn worker, which is the basic uvicorn packaging.
Technical Context
Redis is already used by quantara/web_app/contract_tools/cache.py (see
get_redis_pool()). Pool is created lazily with redis.asyncio. Add a
thin NonceStore class to wallet_auth.py (or api/session.py) that
implements the same three methods, but read/write through Redis. Use
SET nonce EX 300 NX to issue and DEL to consume, where
NX guarantees no two workers can return the same nonce.
Expected Outcome
Nonce issuance/consumption is consistent across worker boundaries and
survives a deploy.
Acceptance Criteria
Restarting uvicorn mid-flight: outstanding nonces return 401 to any caller (auto-expired / non-existent).
Stress: 100 concurrent /nonce for the same wallet → 100 unique nonces (no duplicates).
Race: 2 concurrent signed POSTs with same nonce → exactly one 200, one 401.
tests/test_wallet_auth.py updated to use fakeredis.aioredis instead of the module-level dict.
Implementation Notes
Use redis.asyncio (get_redis_pool() already exists). Key shape:
quantara:nonce:{nonce} = wallet_id with EX=300. Never log the
nonce itself — they are seeds for replay attacks if leaked.
Files / Modules Affected
quantara/web_app/api/wallet_auth.py
quantara/web_app/contract_tools/cache.py