79 lines
2.5 KiB
Python
79 lines
2.5 KiB
Python
#!/usr/bin/python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
HTTP Basic Authentication provider
|
||
|
|
"""
|
||
|
|
|
||
|
|
import base64
|
||
|
|
import logging
|
||
|
|
from functools import wraps
|
||
|
|
|
||
|
|
from flask import request
|
||
|
|
|
||
|
|
from .base import AuthProvider
|
||
|
|
|
||
|
|
|
||
|
|
class BasicAuthProvider(AuthProvider):
|
||
|
|
"""HTTP Basic Authentication provider.
|
||
|
|
|
||
|
|
Configuration:
|
||
|
|
username: Required username
|
||
|
|
password: Required password (plain text, or hashed with bcrypt)
|
||
|
|
realm: Authentication realm (default: 'TISBackup')
|
||
|
|
use_bcrypt: If True, password is bcrypt hash (default: False)
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, config):
|
||
|
|
super().__init__(config)
|
||
|
|
self.logger = logging.getLogger("tisbackup.auth")
|
||
|
|
self.username = config.get("username")
|
||
|
|
self.password = config.get("password")
|
||
|
|
self.realm = config.get("realm", "TISBackup")
|
||
|
|
self.use_bcrypt = config.get("use_bcrypt", False)
|
||
|
|
|
||
|
|
if not self.username or not self.password:
|
||
|
|
raise ValueError("BasicAuth requires 'username' and 'password' in config")
|
||
|
|
|
||
|
|
if self.use_bcrypt:
|
||
|
|
try:
|
||
|
|
import bcrypt
|
||
|
|
self.bcrypt = bcrypt
|
||
|
|
except ImportError:
|
||
|
|
raise ImportError("bcrypt library required for password hashing. Install with: pip install bcrypt")
|
||
|
|
|
||
|
|
def is_authenticated(self):
|
||
|
|
"""Check if request has valid Basic Auth credentials."""
|
||
|
|
auth = request.authorization
|
||
|
|
|
||
|
|
if not auth:
|
||
|
|
return False
|
||
|
|
|
||
|
|
if auth.username != self.username:
|
||
|
|
self.logger.warning(f"Failed authentication attempt for user: {auth.username}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
if self.use_bcrypt:
|
||
|
|
# Compare bcrypt hash
|
||
|
|
password_bytes = auth.password.encode('utf-8')
|
||
|
|
hash_bytes = self.password.encode('utf-8') if isinstance(self.password, str) else self.password
|
||
|
|
return self.bcrypt.checkpw(password_bytes, hash_bytes)
|
||
|
|
else:
|
||
|
|
# Plain text comparison (not recommended for production)
|
||
|
|
return auth.password == self.password
|
||
|
|
|
||
|
|
def handle_unauthorized(self):
|
||
|
|
"""Return 401 with WWW-Authenticate header."""
|
||
|
|
from flask import Response
|
||
|
|
return Response(
|
||
|
|
'Authentication required',
|
||
|
|
401,
|
||
|
|
{'WWW-Authenticate': f'Basic realm="{self.realm}"'}
|
||
|
|
)
|
||
|
|
|
||
|
|
def get_current_user(self):
|
||
|
|
"""Get current authenticated user."""
|
||
|
|
auth = request.authorization
|
||
|
|
if auth and self.is_authenticated():
|
||
|
|
return {"username": auth.username, "auth_type": "basic"}
|
||
|
|
return None
|