Compare commits

..

2 Commits

Author SHA1 Message Date
e9dcf8b5dc Merge pull request 'Add user authentication system' (#1) from feature/user-authentication into main
Reviewed-on: #1
2025-08-25 21:57:42 +02:00
Alexander Domene
31c42ca7ae Add user authentication system
- Implement secure user registration and login
- Add password hashing with PBKDF2 and random salts
- Create session-based authentication with secure tokens
- Support user deactivation and session management
- Include comprehensive unit tests for authentication
- Integrate authentication demo into main application

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 20:40:05 +02:00
3 changed files with 283 additions and 0 deletions

22
main.py
View File

@@ -6,6 +6,7 @@ Main application entry point for the sandbox project.
import json import json
import os import os
from datetime import datetime from datetime import datetime
from src.auth import UserManager
def load_config(): def load_config():
"""Load configuration from config.json""" """Load configuration from config.json"""
@@ -25,5 +26,26 @@ def main():
if config.get("debug"): if config.get("debug"):
print("Running in debug mode") print("Running in debug mode")
# Initialize authentication system
auth_manager = UserManager()
print("\n--- Authentication System Demo ---")
try:
# Register a demo user
auth_manager.register_user("demo_user", "demo@example.com", "secure_password123")
print("✓ Demo user registered successfully")
# Authenticate the user
session_token = auth_manager.authenticate("demo_user", "secure_password123")
print("✓ User authenticated successfully")
print(f"Session token: {session_token[:20]}...")
# Validate session
username = auth_manager.validate_session(session_token)
print(f"✓ Session validated for user: {username}")
except Exception as e:
print(f"✗ Authentication error: {e}")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

120
src/auth.py Normal file
View File

@@ -0,0 +1,120 @@
"""
User authentication module for the sandbox project
"""
import hashlib
import secrets
import time
from typing import Dict, Optional, Tuple
class User:
"""Represents a user in the system"""
def __init__(self, username: str, email: str, password_hash: str):
self.username = username
self.email = email
self.password_hash = password_hash
self.created_at = time.time()
self.last_login = None
self.is_active = True
class AuthenticationError(Exception):
"""Raised when authentication fails"""
pass
class UserManager:
"""Manages user authentication and registration"""
def __init__(self):
self.users: Dict[str, User] = {}
self.sessions: Dict[str, str] = {} # session_token -> username
def _hash_password(self, password: str, salt: str = None) -> Tuple[str, str]:
"""Hash a password with salt"""
if salt is None:
salt = secrets.token_hex(16)
password_hash = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt.encode('utf-8'),
100000 # iterations
)
return password_hash.hex(), salt
def register_user(self, username: str, email: str, password: str) -> bool:
"""Register a new user"""
if username in self.users:
raise ValueError("Username already exists")
if len(password) < 8:
raise ValueError("Password must be at least 8 characters long")
password_hash, salt = self._hash_password(password)
full_hash = f"{salt}:{password_hash}"
user = User(username, email, full_hash)
self.users[username] = user
return True
def authenticate(self, username: str, password: str) -> Optional[str]:
"""Authenticate a user and return session token"""
if username not in self.users:
raise AuthenticationError("Invalid username or password")
user = self.users[username]
if not user.is_active:
raise AuthenticationError("Account is deactivated")
# Extract salt and hash from stored password
try:
salt, stored_hash = user.password_hash.split(':', 1)
except ValueError:
raise AuthenticationError("Invalid password format")
# Hash the provided password with the stored salt
password_hash, _ = self._hash_password(password, salt)
if password_hash != stored_hash:
raise AuthenticationError("Invalid username or password")
# Generate session token
session_token = secrets.token_urlsafe(32)
self.sessions[session_token] = username
# Update last login
user.last_login = time.time()
return session_token
def validate_session(self, session_token: str) -> Optional[str]:
"""Validate a session token and return username"""
return self.sessions.get(session_token)
def logout(self, session_token: str) -> bool:
"""Logout a user by invalidating their session"""
if session_token in self.sessions:
del self.sessions[session_token]
return True
return False
def get_user(self, username: str) -> Optional[User]:
"""Get user information"""
return self.users.get(username)
def deactivate_user(self, username: str) -> bool:
"""Deactivate a user account"""
if username in self.users:
self.users[username].is_active = False
# Remove all sessions for this user
sessions_to_remove = [
token for token, user in self.sessions.items()
if user == username
]
for token in sessions_to_remove:
del self.sessions[token]
return True
return False

141
tests/unit/test_auth.py Normal file
View File

@@ -0,0 +1,141 @@
import pytest
import sys
import os
import time
# Add src directory to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src'))
from auth import UserManager, AuthenticationError, User
class TestUserManager:
def setup_method(self):
"""Setup test fixtures"""
self.user_manager = UserManager()
def test_register_user_success(self):
"""Test successful user registration"""
result = self.user_manager.register_user("testuser", "test@example.com", "password123")
assert result is True
assert "testuser" in self.user_manager.users
user = self.user_manager.users["testuser"]
assert user.username == "testuser"
assert user.email == "test@example.com"
assert user.is_active is True
assert user.created_at is not None
def test_register_duplicate_username(self):
"""Test registration with duplicate username"""
self.user_manager.register_user("testuser", "test1@example.com", "password123")
with pytest.raises(ValueError, match="Username already exists"):
self.user_manager.register_user("testuser", "test2@example.com", "password456")
def test_register_weak_password(self):
"""Test registration with weak password"""
with pytest.raises(ValueError, match="Password must be at least 8 characters long"):
self.user_manager.register_user("testuser", "test@example.com", "123")
def test_authenticate_success(self):
"""Test successful authentication"""
self.user_manager.register_user("testuser", "test@example.com", "password123")
session_token = self.user_manager.authenticate("testuser", "password123")
assert session_token is not None
assert len(session_token) > 0
assert session_token in self.user_manager.sessions
assert self.user_manager.sessions[session_token] == "testuser"
def test_authenticate_invalid_username(self):
"""Test authentication with invalid username"""
with pytest.raises(AuthenticationError, match="Invalid username or password"):
self.user_manager.authenticate("nonexistent", "password123")
def test_authenticate_invalid_password(self):
"""Test authentication with invalid password"""
self.user_manager.register_user("testuser", "test@example.com", "password123")
with pytest.raises(AuthenticationError, match="Invalid username or password"):
self.user_manager.authenticate("testuser", "wrongpassword")
def test_authenticate_deactivated_user(self):
"""Test authentication with deactivated user"""
self.user_manager.register_user("testuser", "test@example.com", "password123")
self.user_manager.deactivate_user("testuser")
with pytest.raises(AuthenticationError, match="Account is deactivated"):
self.user_manager.authenticate("testuser", "password123")
def test_validate_session_success(self):
"""Test successful session validation"""
self.user_manager.register_user("testuser", "test@example.com", "password123")
session_token = self.user_manager.authenticate("testuser", "password123")
username = self.user_manager.validate_session(session_token)
assert username == "testuser"
def test_validate_session_invalid_token(self):
"""Test session validation with invalid token"""
username = self.user_manager.validate_session("invalid_token")
assert username is None
def test_logout_success(self):
"""Test successful logout"""
self.user_manager.register_user("testuser", "test@example.com", "password123")
session_token = self.user_manager.authenticate("testuser", "password123")
result = self.user_manager.logout(session_token)
assert result is True
assert session_token not in self.user_manager.sessions
def test_logout_invalid_token(self):
"""Test logout with invalid token"""
result = self.user_manager.logout("invalid_token")
assert result is False
def test_get_user_success(self):
"""Test getting user information"""
self.user_manager.register_user("testuser", "test@example.com", "password123")
user = self.user_manager.get_user("testuser")
assert user is not None
assert user.username == "testuser"
assert user.email == "test@example.com"
def test_get_user_nonexistent(self):
"""Test getting nonexistent user"""
user = self.user_manager.get_user("nonexistent")
assert user is None
def test_deactivate_user_success(self):
"""Test successful user deactivation"""
self.user_manager.register_user("testuser", "test@example.com", "password123")
session_token = self.user_manager.authenticate("testuser", "password123")
result = self.user_manager.deactivate_user("testuser")
assert result is True
assert self.user_manager.users["testuser"].is_active is False
assert session_token not in self.user_manager.sessions
def test_deactivate_user_nonexistent(self):
"""Test deactivating nonexistent user"""
result = self.user_manager.deactivate_user("nonexistent")
assert result is False
def test_password_hashing_security(self):
"""Test that passwords are properly hashed and salted"""
self.user_manager.register_user("user1", "user1@example.com", "password123")
self.user_manager.register_user("user2", "user2@example.com", "password123")
user1 = self.user_manager.users["user1"]
user2 = self.user_manager.users["user2"]
# Same password should have different hashes due to different salts
assert user1.password_hash != user2.password_hash
# Password hash should contain salt and hash separated by colon
assert ":" in user1.password_hash
assert ":" in user2.password_hash