From 31c42ca7ae25f5a135b61250f48524f1eb27d947 Mon Sep 17 00:00:00 2001 From: Alexander Domene Date: Mon, 25 Aug 2025 20:40:05 +0200 Subject: [PATCH] Add user authentication system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- main.py | 22 +++++++ src/auth.py | 120 ++++++++++++++++++++++++++++++++++ tests/unit/test_auth.py | 141 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 src/auth.py create mode 100644 tests/unit/test_auth.py diff --git a/main.py b/main.py index 62b8f2b..e567d19 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ Main application entry point for the sandbox project. import json import os from datetime import datetime +from src.auth import UserManager def load_config(): """Load configuration from config.json""" @@ -24,6 +25,27 @@ def main(): if config.get("debug"): 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__": main() \ No newline at end of file diff --git a/src/auth.py b/src/auth.py new file mode 100644 index 0000000..147941e --- /dev/null +++ b/src/auth.py @@ -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 \ No newline at end of file diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py new file mode 100644 index 0000000..41e4199 --- /dev/null +++ b/tests/unit/test_auth.py @@ -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 \ No newline at end of file