Phase 2 Step 4: Implement Authentication System
Implemented comprehensive Flask-Login authentication with single-user support. New Features: - Flask-Login integration with User model - Bcrypt password hashing via PasswordManager - Login, logout, and initial password setup routes - @login_required and @api_auth_required decorators - All API endpoints now require authentication - Bootstrap 5 dark theme UI templates - Dashboard with navigation - Remember me and next parameter redirect support Files Created (12): - web/auth/__init__.py, models.py, decorators.py, routes.py - web/routes/__init__.py, main.py - web/templates/login.html, setup.html, dashboard.html, scans.html, scan_detail.html - tests/test_authentication.py (30+ tests) Files Modified (6): - web/app.py: Added Flask-Login initialization and main routes - web/api/scans.py: Protected all endpoints with @api_auth_required - web/api/settings.py: Protected all endpoints with @api_auth_required - web/api/schedules.py: Protected all endpoints with @api_auth_required - web/api/alerts.py: Protected all endpoints with @api_auth_required - tests/conftest.py: Added authentication test fixtures Security: - Session-based authentication for both web UI and API - Secure password storage with bcrypt - Protected routes redirect to login page - Protected API endpoints return 401 Unauthorized - Health check endpoints remain accessible for monitoring Testing: - User model authentication and properties - Login success/failure flows - Logout and session management - Password setup workflow - API endpoint authentication requirements - Session persistence and remember me functionality - Next parameter redirect behavior Total: ~1,200 lines of code added 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
107
web/auth/models.py
Normal file
107
web/auth/models.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
User model for Flask-Login authentication.
|
||||
|
||||
Simple single-user model that loads credentials from the settings table.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from web.utils.settings import PasswordManager, SettingsManager
|
||||
|
||||
|
||||
class User(UserMixin):
|
||||
"""
|
||||
User class for Flask-Login.
|
||||
|
||||
Represents the single application user. Credentials are stored in the
|
||||
settings table (app_password key).
|
||||
"""
|
||||
|
||||
# Single user ID (always 1 for single-user app)
|
||||
USER_ID = '1'
|
||||
|
||||
def __init__(self, user_id: str = USER_ID):
|
||||
"""
|
||||
Initialize user.
|
||||
|
||||
Args:
|
||||
user_id: User ID (always '1' for single-user app)
|
||||
"""
|
||||
self.id = user_id
|
||||
|
||||
def get_id(self) -> str:
|
||||
"""
|
||||
Get user ID for Flask-Login.
|
||||
|
||||
Returns:
|
||||
User ID string
|
||||
"""
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def is_authenticated(self) -> bool:
|
||||
"""User is always authenticated if instance exists."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
"""User is always active."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_anonymous(self) -> bool:
|
||||
"""User is never anonymous."""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get(user_id: str, db_session: Session = None) -> Optional['User']:
|
||||
"""
|
||||
Get user by ID (Flask-Login user_loader).
|
||||
|
||||
Args:
|
||||
user_id: User ID to load
|
||||
db_session: Database session (unused - kept for compatibility)
|
||||
|
||||
Returns:
|
||||
User instance if ID is valid, None otherwise
|
||||
"""
|
||||
if user_id == User.USER_ID:
|
||||
return User(user_id)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def authenticate(password: str, db_session: Session) -> Optional['User']:
|
||||
"""
|
||||
Authenticate user with password.
|
||||
|
||||
Args:
|
||||
password: Password to verify
|
||||
db_session: Database session for accessing settings
|
||||
|
||||
Returns:
|
||||
User instance if password is correct, None otherwise
|
||||
"""
|
||||
settings_manager = SettingsManager(db_session)
|
||||
|
||||
if PasswordManager.verify_app_password(settings_manager, password):
|
||||
return User(User.USER_ID)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def has_password_set(db_session: Session) -> bool:
|
||||
"""
|
||||
Check if application password is set.
|
||||
|
||||
Args:
|
||||
db_session: Database session for accessing settings
|
||||
|
||||
Returns:
|
||||
True if password is set, False otherwise
|
||||
"""
|
||||
settings_manager = SettingsManager(db_session)
|
||||
stored_hash = settings_manager.get('app_password', decrypt=False)
|
||||
return bool(stored_hash)
|
||||
Reference in New Issue
Block a user