Files
Church-Music/legacy-site/backend/biometric_auth.py

381 lines
14 KiB
Python

"""
Biometric Authentication Module
Simplified: Use device fingerprint + biometric to unlock stored credentials
"""
import hashlib
import secrets
from datetime import datetime
from postgresql_models import SessionLocal, BiometricCredential
import logging
logger = logging.getLogger(__name__)
def register_biometric_credential(username, device_name, device_fingerprint, device_info):
"""Register a biometric-enabled device for a user"""
from postgresql_models import User
db = SessionLocal()
try:
# Check if this device is already registered for this user
existing = db.query(BiometricCredential).filter_by(
username=username,
device_fingerprint=device_fingerprint
).first()
if existing:
# Update existing
existing.device_name = device_name or existing.device_name
existing.device_info = device_info or existing.device_info
existing.enabled = 1
existing.last_used = datetime.now()
db.commit()
logger.info(f"Updated biometric for {username} on device {device_name}")
return {'success': True, 'message': 'Biometric updated'}
# Find user in User table
user = db.query(User).filter_by(username=username).first()
user_id = user.id if user else None
# Create new biometric registration
credential = BiometricCredential(
username=username,
user_id=user_id,
credential_id=device_fingerprint, # Use device fingerprint as ID
device_fingerprint=device_fingerprint,
public_key='', # Not used in this simplified approach
device_name=device_name or 'Unknown Device',
device_info=device_info or '',
enabled=1,
created_at=datetime.now(),
last_used=datetime.now()
)
db.add(credential)
db.commit()
logger.info(f"Registered biometric for {username} on device {device_name}")
return {
'success': True,
'message': 'Biometric registered successfully'
}
except Exception as e:
db.rollback()
logger.error(f"Error registering biometric: {e}")
return {'success': False, 'error': str(e)}
finally:
db.close()
def authenticate_biometric_by_device(username, device_fingerprint):
"""
Authenticate user by verifying device fingerprint
The browser already verified the biometric, we just check if this device is registered
If username is None, lookup by device fingerprint alone
"""
from postgresql_models import User
db = SessionLocal()
try:
logger.info(f"Biometric auth for {username or 'unknown'} from device {device_fingerprint}")
# Find biometric registration - either by username+fingerprint or just fingerprint
if username:
credential = db.query(BiometricCredential).filter_by(
username=username,
device_fingerprint=device_fingerprint,
enabled=1
).first()
else:
# No username provided - lookup by device fingerprint alone
credential = db.query(BiometricCredential).filter_by(
device_fingerprint=device_fingerprint,
enabled=1
).first()
if not credential:
logger.warning(f"No biometric found for device {device_fingerprint}")
return {'success': False, 'error': 'No biometric setup found for this device'}
# Get username from credential if not provided
if not username:
username = credential.username
logger.info(f"Found username {username} from device fingerprint")
# Get user details
user = db.query(User).filter_by(username=username).first()
if not user:
logger.error(f"User {username} not found in database")
return {'success': False, 'error': 'User not found'}
# Update last used
credential.last_used = datetime.now()
db.commit()
logger.info(f"Biometric auth successful for {username}")
return {
'success': True,
'username': user.username,
'role': user.role,
'permissions': user.get_permissions_list()
}
except Exception as e:
logger.error(f"Error in biometric authentication: {e}", exc_info=True)
return {'success': False, 'error': str(e)}
finally:
db.close()
def get_user_biometric_status(username):
"""Check if user has biometric enabled"""
db = SessionLocal()
try:
credentials = db.query(BiometricCredential).filter_by(
username=username,
enabled=1
).all()
return {
'success': True,
'has_biometric': len(credentials) > 0,
'device_count': len(credentials),
'devices': [{
'device_name': c.device_name,
'last_used': c.last_used.isoformat() if c.last_used else None
} for c in credentials]
}
except Exception as e:
logger.error(f"Error checking biometric status: {e}")
return {'success': False, 'error': str(e)}
finally:
db.close()
"""Register a new biometric credential for a user"""
from postgresql_models import User # Import here to avoid circular import
db = SessionLocal()
try:
# Check if credential already exists
existing = db.query(BiometricCredential).filter_by(credential_id=credential_id).first()
if existing:
return {'success': False, 'error': 'credential_exists'}
# Find user in User table
user = db.query(User).filter_by(username=username).first()
user_id = user.id if user else None
# Create new credential
credential = BiometricCredential(
username=username,
user_id=user_id, # Link to User table if exists
credential_id=credential_id,
public_key=public_key,
device_name=device_name or 'Unknown Device',
device_info=device_info or '',
enabled=1,
created_at=datetime.now(),
last_used=datetime.now()
)
db.add(credential)
db.commit()
db.refresh(credential)
return {
'success': True,
'credential': {
'id': credential.id,
'device_name': credential.device_name,
'created_at': credential.created_at.isoformat()
}
}
except Exception as e:
db.rollback()
logger.error(f"Error registering biometric credential: {e}")
return {'success': False, 'error': str(e)}
finally:
db.close()
def authenticate_biometric(username, credential_id, signature, challenge):
"""Authenticate using biometric credential"""
db = SessionLocal()
try:
logger.info(f"Authenticating biometric for user: {username}")
logger.info(f"Credential ID (first 40 chars): {credential_id[:40] if credential_id else 'None'}...")
logger.info(f"Credential ID length: {len(credential_id) if credential_id else 0}")
# First, check if user has any credentials at all
all_credentials = db.query(BiometricCredential).filter_by(username=username).all()
logger.info(f"User {username} has {len(all_credentials)} total credentials in database")
if all_credentials:
for idx, cred in enumerate(all_credentials):
logger.info(f" Credential {idx+1}: ID (first 40): {cred.credential_id[:40]}..., enabled: {cred.enabled}, device: {cred.device_name}")
# Find credential - match by username first, then credential_id
credential = db.query(BiometricCredential).filter_by(
username=username,
credential_id=credential_id,
enabled=1
).first()
if not credential:
logger.warning(f"No matching enabled credential found for {username}")
# Try to find any enabled credential for debugging
any_enabled = db.query(BiometricCredential).filter_by(username=username, enabled=1).first()
if any_enabled:
logger.warning(f"User has enabled credential but ID doesn't match.")
logger.warning(f" Expected (first 40): {any_enabled.credential_id[:40]}...")
logger.warning(f" Received (first 40): {credential_id[:40]}...")
else:
logger.warning(f"User has no enabled credentials at all")
return {'success': False, 'error': 'credential_not_found'}
logger.info(f"Credential found for {username} on device: {credential.device_name}")
# Verify signature (currently simplified - accepts all)
if not verify_signature(credential.public_key, signature, challenge):
logger.error(f"Signature verification failed for {username}")
return {'success': False, 'error': 'invalid_signature'}
# Update last used timestamp
credential.last_used = datetime.now()
db.commit()
logger.info(f"Biometric authentication successful for {username}")
return {
'success': True,
'username': username,
'device_name': credential.device_name
}
except Exception as e:
logger.error(f"Error authenticating biometric: {e}", exc_info=True)
return {'success': False, 'error': str(e)}
finally:
db.close()
def authenticate_biometric_by_credential(credential_id, signature, challenge):
"""
Authenticate using biometric credential - looks up username from credential ID
This is the proper WebAuthn flow where the credential identifies the user
"""
db = SessionLocal()
try:
logger.info(f"Authenticating by credential ID (first 40): {credential_id[:40]}...")
logger.info(f"Credential ID length: {len(credential_id)}")
# Find credential by ID only (no username needed)
credential = db.query(BiometricCredential).filter_by(
credential_id=credential_id,
enabled=1
).first()
if not credential:
logger.warning(f"No enabled credential found with this ID")
# Debug: show all credentials
all_creds = db.query(BiometricCredential).filter_by(enabled=1).all()
logger.info(f"Total enabled credentials in database: {len(all_creds)}")
for idx, cred in enumerate(all_creds[:5]): # Show first 5
logger.info(f" Cred {idx+1}: User={cred.username}, ID (first 40)={cred.credential_id[:40]}...")
return {'success': False, 'error': 'credential_not_found'}
username = credential.username
logger.info(f"Credential found! Belongs to user: {username}, device: {credential.device_name}")
# Verify signature
if not verify_signature(credential.public_key, signature, challenge):
logger.error(f"Signature verification failed for credential")
return {'success': False, 'error': 'invalid_signature'}
# Update last used timestamp
credential.last_used = datetime.now()
db.commit()
logger.info(f"Biometric authentication successful for {username}")
return {
'success': True,
'username': username,
'device_name': credential.device_name
}
except Exception as e:
logger.error(f"Error authenticating biometric by credential: {e}", exc_info=True)
return {'success': False, 'error': str(e)}
finally:
db.close()
def get_user_credentials(username):
"""Get all biometric credentials for a user"""
db = SessionLocal()
try:
credentials = db.query(BiometricCredential).filter_by(username=username).all()
return {
'success': True,
'credentials': [{
'id': c.id,
'device_name': c.device_name,
'device_info': c.device_info,
'enabled': bool(c.enabled),
'created_at': c.created_at.isoformat(),
'last_used': c.last_used.isoformat() if c.last_used else None
} for c in credentials]
}
except Exception as e:
logger.error(f"Error fetching credentials: {e}")
return {'success': False, 'error': str(e)}
finally:
db.close()
def toggle_credential(credential_id, enabled):
"""Enable or disable a biometric credential"""
db = SessionLocal()
try:
credential = db.query(BiometricCredential).filter_by(id=credential_id).first()
if not credential:
return {'success': False, 'error': 'credential_not_found'}
credential.enabled = 1 if enabled else 0
db.commit()
return {'success': True, 'enabled': bool(credential.enabled)}
except Exception as e:
db.rollback()
logger.error(f"Error toggling credential: {e}")
return {'success': False, 'error': str(e)}
finally:
db.close()
def delete_credential(credential_id):
"""Delete a biometric credential"""
db = SessionLocal()
try:
credential = db.query(BiometricCredential).filter_by(id=credential_id).first()
if not credential:
return {'success': False, 'error': 'credential_not_found'}
db.delete(credential)
db.commit()
return {'success': True}
except Exception as e:
db.rollback()
logger.error(f"Error deleting credential: {e}")
return {'success': False, 'error': str(e)}
finally:
db.close()
def delete_all_user_biometrics(username):
"""Delete all biometric credentials for a user"""
db = SessionLocal()
try:
# Delete all credentials for this user
deleted_count = db.query(BiometricCredential).filter_by(username=username).delete()
db.commit()
logger.info(f"Deleted {deleted_count} biometric credential(s) for user {username}")
return {'success': True, 'deleted_count': deleted_count}
except Exception as e:
db.rollback()
logger.error(f"Error deleting biometrics for {username}: {e}")
return {'success': False, 'error': str(e)}
finally:
db.close()