TISbackup/libtisbackup/auth/flask_login_auth.py
k3nny f12d89f3da feat(auth): add pluggable authentication system for Flask routes
Implement comprehensive authentication system with support for
Basic Auth, Flask-Login, and OAuth2 providers.

Features:
- Pluggable architecture via factory pattern
- Multiple authentication providers:
  * None: No authentication (development/testing)
  * Basic Auth: HTTP Basic with bcrypt support
  * Flask-Login: Session-based with multiple users
  * OAuth2: Google, GitHub, GitLab, and generic providers
- Decorator-based route protection (@auth.require_auth)
- User authorization by domain or email (OAuth)
- bcrypt password hashing support
- Comprehensive documentation and examples

Components:
- libtisbackup/auth/__init__.py: Factory function and exports
- libtisbackup/auth/base.py: Base provider interface
- libtisbackup/auth/basic_auth.py: HTTP Basic Auth implementation
- libtisbackup/auth/flask_login_auth.py: Flask-Login implementation
- libtisbackup/auth/oauth_auth.py: OAuth2 implementation
- libtisbackup/auth/example_integration.py: Integration examples
- libtisbackup/auth/README.md: API reference and examples

Documentation:
- AUTHENTICATION.md: Complete authentication guide
  * Setup instructions for each provider
  * Configuration examples
  * Security best practices
  * Troubleshooting guide
  * Migration guide
- samples/auth-config-examples.ini: Configuration templates

Dependencies:
- Add optional dependencies in pyproject.toml:
  * auth-basic: bcrypt>=4.0.0
  * auth-login: flask-login>=0.6.0, bcrypt>=4.0.0
  * auth-oauth: authlib>=1.3.0, requests>=2.32.0
  * auth-all: All auth providers

Installation:
```bash
# Install specific provider
uv sync --extra auth-basic

# Install all providers
uv sync --extra auth-all
```

Usage:
```python
from libtisbackup.auth import get_auth_provider

# Initialize
auth = get_auth_provider("basic", {
    "username": "admin",
    "password": "$2b$12$...",
    "use_bcrypt": True
})
auth.init_app(app)

# Protect routes
@app.route("/")
@auth.require_auth
def index():
    user = auth.get_current_user()
    return f"Hello {user['username']}"
```

Security features:
- bcrypt password hashing (work factor 12)
- OAuth domain/user restrictions
- Session-based authentication
- Clear separation of concerns
- Environment variable support for secrets

OAuth providers supported:
- Google (OpenID Connect)
- GitHub
- GitLab
- Generic OAuth2 provider

Breaking change: None - new feature, backward compatible
Users can continue without authentication (type=none)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 02:02:46 +02:00

157 lines
4.7 KiB
Python

#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Flask-Login authentication provider
"""
import logging
from functools import wraps
from flask import redirect, request, session, url_for
from .base import AuthProvider
class FlaskLoginProvider(AuthProvider):
"""Flask-Login based authentication provider.
Configuration:
users: Dict of {username: password_hash} or path to user file
secret_key: Flask secret key for sessions
login_view: Route name for login page (default: 'login')
use_bcrypt: If True, passwords are bcrypt hashes (default: True)
"""
def __init__(self, config):
super().__init__(config)
self.logger = logging.getLogger("tisbackup.auth")
self.login_manager = None
self.users = config.get("users", {})
self.login_view = config.get("login_view", "login")
self.use_bcrypt = config.get("use_bcrypt", True)
if self.use_bcrypt:
try:
import bcrypt
self.bcrypt = bcrypt
except ImportError:
raise ImportError("bcrypt library required. Install with: pip install bcrypt")
# Load users from file if path provided
users_file = config.get("users_file")
if users_file:
self._load_users_from_file(users_file)
def _load_users_from_file(self, filepath):
"""Load users from file (username:password_hash per line)."""
try:
with open(filepath) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
username, password_hash = line.split(':', 1)
self.users[username.strip()] = password_hash.strip()
except Exception as e:
self.logger.error(f"Failed to load users from {filepath}: {e}")
raise
def init_app(self, app):
"""Initialize Flask-Login with the app."""
try:
from flask_login import LoginManager, UserMixin
except ImportError:
raise ImportError("flask-login library required. Install with: pip install flask-login")
self.login_manager = LoginManager()
self.login_manager.init_app(app)
self.login_manager.login_view = self.login_view
# Simple User class
class User(UserMixin):
def __init__(self, username):
self.id = username
self.username = username
self.User = User
@self.login_manager.user_loader
def load_user(user_id):
if user_id in self.users:
return User(user_id)
return None
def verify_password(self, username, password):
"""Verify username and password.
Args:
username: Username to check
password: Password to verify
Returns:
bool: True if valid, False otherwise
"""
if username not in self.users:
return False
stored_hash = self.users[username]
if self.use_bcrypt:
password_bytes = password.encode('utf-8')
hash_bytes = stored_hash.encode('utf-8') if isinstance(stored_hash, str) else stored_hash
return self.bcrypt.checkpw(password_bytes, hash_bytes)
else:
return password == stored_hash
def login_user(self, username):
"""Login a user by username.
Args:
username: Username to login
Returns:
bool: True if successful
"""
try:
from flask_login import login_user
except ImportError:
return False
if username in self.users:
user = self.User(username)
login_user(user)
return True
return False
def is_authenticated(self):
"""Check if current user is authenticated."""
try:
from flask_login import current_user
return current_user.is_authenticated
except ImportError:
return False
def handle_unauthorized(self):
"""Redirect to login page."""
return redirect(url_for(self.login_view))
def get_current_user(self):
"""Get current authenticated user."""
try:
from flask_login import current_user
if current_user.is_authenticated:
return {
"username": current_user.username,
"auth_type": "flask-login"
}
except ImportError:
pass
return None
def logout(self):
"""Logout current user."""
try:
from flask_login import logout_user
logout_user()
except ImportError:
pass