Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/web_client_nonce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import secrets
import time
from typing import Dict, Any

class WebClientNonceManager:
"""
A class to manage nonce generation and validation for web clients.

This class provides methods to generate unique, time-limited nonces
that can be used for security purposes such as preventing replay attacks.
"""

def __init__(self, nonce_expiry_seconds: int = 300):
"""
Initialize the WebClientNonceManager.

Args:
nonce_expiry_seconds (int, optional): Time in seconds after which
a nonce becomes invalid.
Defaults to 300 seconds (5 minutes).
"""
self._nonce_store: Dict[str, Dict[str, Any]] = {}
self._nonce_expiry = nonce_expiry_seconds

def generate_nonce(self, client_id: str) -> str:
"""
Generate a unique nonce for a given client.

Args:
client_id (str): Unique identifier for the client.

Returns:
str: A unique nonce string.
"""
# Generate a cryptographically secure random nonce
nonce = secrets.token_urlsafe(32)

# Store nonce with timestamp
self._nonce_store[nonce] = {
'client_id': client_id,
'timestamp': time.time()
}

# Clean up expired nonces
self._cleanup_expired_nonces()

return nonce

def validate_nonce(self, nonce: str, client_id: str) -> bool:
"""
Validate a nonce for a specific client.

Args:
nonce (str): The nonce to validate.
client_id (str): The client ID associated with the nonce.

Returns:
bool: True if nonce is valid, False otherwise.
"""
# Check if nonce exists and belongs to the client
if nonce not in self._nonce_store:
return False

stored_nonce_data = self._nonce_store[nonce]

# Check client ID match
if stored_nonce_data['client_id'] != client_id:
return False

# Check nonce age
current_time = time.time()
nonce_age = current_time - stored_nonce_data['timestamp']

if nonce_age > self._nonce_expiry:
# Remove expired nonce
del self._nonce_store[nonce]
return False

# Remove used nonce to prevent replay attacks
del self._nonce_store[nonce]

return True

def _cleanup_expired_nonces(self) -> None:
"""
Remove expired nonces from the storage.

Nonces older than the configured expiry time are removed.
"""
current_time = time.time()
expired_nonces = [
nonce for nonce, data in self._nonce_store.items()
if current_time - data['timestamp'] > self._nonce_expiry
]

for nonce in expired_nonces:
del self._nonce_store[nonce]
51 changes: 51 additions & 0 deletions tests/test_web_client_nonce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import time
import pytest
from src.web_client_nonce import WebClientNonceManager

def test_nonce_generation():
"""Test nonce generation creates unique values."""
nonce_manager = WebClientNonceManager()
client_id = 'test_client'

nonce1 = nonce_manager.generate_nonce(client_id)
nonce2 = nonce_manager.generate_nonce(client_id)

assert nonce1 != nonce2, "Nonces should be unique"

def test_nonce_validation():
"""Test nonce validation works correctly."""
nonce_manager = WebClientNonceManager()
client_id = 'test_client'

nonce = nonce_manager.generate_nonce(client_id)

assert nonce_manager.validate_nonce(nonce, client_id), "Nonce should be valid"
assert not nonce_manager.validate_nonce(nonce, client_id), "Nonce should be invalidated after first use"

def test_nonce_expiry():
"""Test nonce expiry mechanism."""
nonce_manager = WebClientNonceManager(nonce_expiry_seconds=1)
client_id = 'test_client'

nonce = nonce_manager.generate_nonce(client_id)

time.sleep(2) # Wait for nonce to expire

assert not nonce_manager.validate_nonce(nonce, client_id), "Expired nonce should be invalid"

def test_nonce_client_mismatch():
"""Test that nonces cannot be used with different client IDs."""
nonce_manager = WebClientNonceManager()
client_id1 = 'client1'
client_id2 = 'client2'

nonce = nonce_manager.generate_nonce(client_id1)

assert not nonce_manager.validate_nonce(nonce, client_id2), "Nonce should not be valid for different client"

def test_invalid_nonce():
"""Test that invalid nonces are rejected."""
nonce_manager = WebClientNonceManager()
client_id = 'test_client'

assert not nonce_manager.validate_nonce('fake_nonce', client_id), "Invalid nonce should not be validated"