381 lines
14 KiB
Python
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()
|