Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
995 lines
35 KiB
Python
995 lines
35 KiB
Python
"""
|
|
Authentication API endpoints for multi-tenant SaaS platform.
|
|
|
|
Provides REST API endpoints for:
|
|
- User registration and login
|
|
- Multi-method authentication
|
|
- MFA verification and management
|
|
- Token management and refresh
|
|
- Password management
|
|
- Social authentication
|
|
- Magic link authentication
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, Any, Optional
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.models import AnonymousUser
|
|
from django.utils import timezone
|
|
from django.core.cache import cache
|
|
from rest_framework import status, generics, viewsets
|
|
from rest_framework.decorators import action, api_view, permission_classes
|
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
|
from rest_framework.response import Response
|
|
from rest_framework.request import Request
|
|
from rest_framework_simplejwt.tokens import RefreshToken
|
|
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
|
from drf_spectacular.types import OpenApiTypes
|
|
from logging import getLogger
|
|
from ..models.tenant import Tenant
|
|
from ..auth.jwt_service import jwt_service
|
|
from ..auth.authentication import auth_backend
|
|
from ..auth.mfa import mfa_service
|
|
from ..auth.permissions import TenantPermission, HasPermission
|
|
from ..serializers.auth_serializers import (
|
|
LoginSerializer,
|
|
RegisterSerializer,
|
|
MFASerializer,
|
|
MFASetupSerializer,
|
|
TokenRefreshSerializer,
|
|
PasswordChangeSerializer,
|
|
PasswordResetSerializer,
|
|
PasswordResetConfirmSerializer,
|
|
MagicLinkSerializer,
|
|
SocialAuthSerializer,
|
|
BiometricAuthSerializer,
|
|
BackupCodeSerializer,
|
|
AuthStatusSerializer,
|
|
)
|
|
from ..services.user_service import user_service
|
|
from ..exceptions import AuthenticationError, ValidationError
|
|
|
|
User = get_user_model()
|
|
logger = getLogger(__name__)
|
|
|
|
|
|
class AuthViewSet(viewsets.GenericViewSet):
|
|
"""
|
|
Authentication API endpoints.
|
|
|
|
Provides comprehensive authentication functionality including:
|
|
- Multi-method login/logout
|
|
- User registration
|
|
- MFA setup and verification
|
|
- Token management
|
|
- Password management
|
|
- Social authentication
|
|
- Magic link authentication
|
|
"""
|
|
|
|
permission_classes = [AllowAny]
|
|
serializer_class = AuthStatusSerializer
|
|
|
|
def get_serializer_class(self):
|
|
"""Return appropriate serializer based on action."""
|
|
if self.action == 'login':
|
|
return LoginSerializer
|
|
elif self.action == 'register':
|
|
return RegisterSerializer
|
|
elif self.action == 'mfa_verify':
|
|
return MFASerializer
|
|
elif self.action == 'mfa_setup':
|
|
return MFASetupSerializer
|
|
elif self.action == 'refresh_token':
|
|
return TokenRefreshSerializer
|
|
elif self.action == 'change_password':
|
|
return PasswordChangeSerializer
|
|
elif self.action == 'reset_password':
|
|
return PasswordResetSerializer
|
|
elif self.action == 'confirm_password_reset':
|
|
return PasswordResetConfirmSerializer
|
|
elif self.action == 'magic_link':
|
|
return MagicLinkSerializer
|
|
elif self.action == 'social_auth':
|
|
return SocialAuthSerializer
|
|
elif self.action == 'biometric_auth':
|
|
return BiometricAuthSerializer
|
|
elif self.action == 'backup_codes':
|
|
return BackupCodeSerializer
|
|
return super().get_serializer_class()
|
|
|
|
@extend_schema(
|
|
summary="User Login",
|
|
description="Authenticate user with multiple methods",
|
|
responses={200: AuthStatusSerializer},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def login(self, request: Request) -> Response:
|
|
"""
|
|
Login user with specified authentication method.
|
|
|
|
Supported methods:
|
|
- password: Email/username + password
|
|
- ic: Malaysian IC number + password
|
|
- company: Company registration + password
|
|
- phone: Phone number + SMS code
|
|
- magic: Magic link token
|
|
- biometric: Biometric authentication
|
|
- google: Google OAuth
|
|
- facebook: Facebook OAuth
|
|
"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
method = serializer.validated_data['method']
|
|
credentials = serializer.validated_data['credentials']
|
|
|
|
# Perform authentication
|
|
auth_result = auth_backend.authenticate(request, method=method, **credentials)
|
|
|
|
# Handle MFA requirement
|
|
if isinstance(auth_result, dict) and auth_result.get('requires_mfa'):
|
|
user = auth_result['user']
|
|
return Response({
|
|
'requires_mfa': True,
|
|
'user_id': str(user.id),
|
|
'email': user.email,
|
|
'mfa_methods': user.get_available_mfa_methods(),
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
# Generate JWT tokens
|
|
user = auth_result
|
|
tenant = getattr(request, 'tenant', None) or getattr(user, 'tenant', None)
|
|
|
|
device_info = {
|
|
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
|
'ip_address': request.META.get('REMOTE_ADDR', ''),
|
|
'device_type': self._get_device_type(request),
|
|
}
|
|
|
|
tokens = jwt_service.generate_token_pair(user, tenant, device_info)
|
|
|
|
# Update user last login
|
|
user.last_login = timezone.now()
|
|
user.save(update_fields=['last_login'])
|
|
|
|
logger.info(f"User {user.id} logged in successfully via {method}")
|
|
|
|
return Response({
|
|
'user': self._serialize_user(user),
|
|
'tokens': tokens,
|
|
'mfa_status': mfa_service.get_mfa_status(user),
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
except AuthenticationError as e:
|
|
logger.warning(f"Login failed: {str(e)}")
|
|
return Response(
|
|
{'error': str(e)},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Login error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Authentication failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="User Registration",
|
|
description="Register new user with tenant context",
|
|
responses={201: AuthStatusSerializer},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def register(self, request: Request) -> Response:
|
|
"""
|
|
Register new user account.
|
|
|
|
Supports individual and tenant-based registration with:
|
|
- Email verification
|
|
- Phone verification (optional)
|
|
- Malaysian IC validation
|
|
- Company registration validation
|
|
"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
# Create user
|
|
user = user_service.create_user(
|
|
email=serializer.validated_data['email'],
|
|
password=serializer.validated_data['password'],
|
|
first_name=serializer.validated_data.get('first_name', ''),
|
|
last_name=serializer.validated_data.get('last_name', ''),
|
|
phone_number=serializer.validated_data.get('phone_number'),
|
|
malaysian_ic=serializer.validated_data.get('malaysian_ic'),
|
|
tenant_id=serializer.validated_data.get('tenant_id'),
|
|
role=serializer.validated_data.get('role', 'user'),
|
|
)
|
|
|
|
# Generate verification codes if required
|
|
verification_codes = {}
|
|
if user.email and not user.email_verified:
|
|
email_code = auth_backend.generate_registration_otp(user.email)
|
|
verification_codes['email_otp'] = email_code['email_otp']
|
|
|
|
if user.phone_number and not user.phone_verified:
|
|
phone_code = auth_backend.generate_phone_verification_code(user.phone_number)
|
|
verification_codes['phone_otp'] = phone_code
|
|
|
|
logger.info(f"User {user.id} registered successfully")
|
|
|
|
return Response({
|
|
'user': self._serialize_user(user),
|
|
'verification_codes': verification_codes,
|
|
'message': 'User registered successfully. Please verify your email.',
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
except ValidationError as e:
|
|
logger.warning(f"Registration validation error: {str(e)}")
|
|
return Response(
|
|
{'error': str(e)},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Registration error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Registration failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="MFA Verification",
|
|
description="Verify MFA code for authentication",
|
|
responses={200: AuthStatusSerializer},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def mfa_verify(self, request: Request) -> Response:
|
|
"""
|
|
Verify MFA code to complete authentication.
|
|
|
|
Supported MFA methods:
|
|
- totp: Time-based One-Time Password
|
|
- sms: SMS verification code
|
|
- email: Email verification code
|
|
- backup: Backup code
|
|
"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
user_id = serializer.validated_data['user_id']
|
|
method = serializer.validated_data['method']
|
|
code = serializer.validated_data['code']
|
|
|
|
user = User.objects.get(id=user_id)
|
|
|
|
# Verify MFA code
|
|
if not mfa_service.validate_mfa_attempt(user, method, code):
|
|
return Response(
|
|
{'error': 'Invalid MFA code'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Generate JWT tokens
|
|
tenant = getattr(request, 'tenant', None) or getattr(user, 'tenant', None)
|
|
device_info = {
|
|
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
|
'ip_address': request.META.get('REMOTE_ADDR', ''),
|
|
'device_type': self._get_device_type(request),
|
|
}
|
|
|
|
tokens = jwt_service.generate_token_pair(user, tenant, device_info)
|
|
|
|
# Update user last login
|
|
user.last_login = timezone.now()
|
|
user.save(update_fields=['last_login'])
|
|
|
|
logger.info(f"User {user.id} MFA verification successful")
|
|
|
|
return Response({
|
|
'user': self._serialize_user(user),
|
|
'tokens': tokens,
|
|
'mfa_status': mfa_service.get_mfa_status(user),
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
except User.DoesNotExist:
|
|
return Response(
|
|
{'error': 'User not found'},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
except AuthenticationError as e:
|
|
return Response(
|
|
{'error': str(e)},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"MFA verification error: {str(e)}")
|
|
return Response(
|
|
{'error': 'MFA verification failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="MFA Setup",
|
|
description="Set up MFA for user account",
|
|
responses={200: MFASetupSerializer},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def mfa_setup(self, request: Request) -> Response:
|
|
"""
|
|
Set up MFA for user account.
|
|
|
|
Supports TOTP setup with QR code generation and verification.
|
|
"""
|
|
if not request.user.is_authenticated:
|
|
return Response(
|
|
{'error': 'Authentication required'},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
user = request.user
|
|
method = serializer.validated_data.get('method', 'totp')
|
|
|
|
if method == 'totp':
|
|
# Generate TOTP setup
|
|
totp_data = mfa_service.setup_totp(user)
|
|
|
|
if 'verification_code' in serializer.validated_data:
|
|
# Verify TOTP setup
|
|
secret = serializer.validated_data['secret']
|
|
code = serializer.validated_data['verification_code']
|
|
|
|
if mfa_service.verify_totp_setup(user, secret, code):
|
|
return Response({
|
|
'message': 'MFA enabled successfully',
|
|
'backup_codes': mfa_service.generate_backup_codes(user),
|
|
})
|
|
else:
|
|
return Response(
|
|
{'error': 'Invalid verification code'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
return Response(totp_data)
|
|
|
|
else:
|
|
return Response(
|
|
{'error': f'MFA method {method} not supported'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"MFA setup error: {str(e)}")
|
|
return Response(
|
|
{'error': 'MFA setup failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="Token Refresh",
|
|
description="Refresh access token using refresh token",
|
|
responses={200: TokenRefreshSerializer},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def refresh_token(self, request: Request) -> Response:
|
|
"""
|
|
Refresh access token using refresh token.
|
|
"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
refresh_token = serializer.validated_data['refresh_token']
|
|
new_tokens = jwt_service.refresh_access_token(refresh_token)
|
|
|
|
return Response(new_tokens, status=status.HTTP_200_OK)
|
|
|
|
except AuthenticationError as e:
|
|
return Response(
|
|
{'error': str(e)},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Token refresh error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Token refresh failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="Logout",
|
|
description="Logout user and blacklist tokens",
|
|
responses={200: {'type': 'object', 'properties': {'message': {'type': 'string'}}}},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def logout(self, request: Request) -> Response:
|
|
"""
|
|
Logout user and blacklist current tokens.
|
|
"""
|
|
if not request.user.is_authenticated:
|
|
return Response(
|
|
{'error': 'Authentication required'},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
|
|
try:
|
|
# Get authorization header
|
|
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
|
if auth_header.startswith('Bearer '):
|
|
token = auth_header.split(' ')[1]
|
|
jwt_service.blacklist_token(token)
|
|
|
|
# Blacklist all user sessions if requested
|
|
blacklist_all = request.data.get('blacklist_all_sessions', False)
|
|
if blacklist_all:
|
|
jwt_service.blacklist_token(token, blacklist_all_sessions=True)
|
|
|
|
logger.info(f"User {request.user.id} logged out successfully")
|
|
|
|
return Response({'message': 'Logged out successfully'}, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Logout error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Logout failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="Change Password",
|
|
description="Change user password",
|
|
responses={200: {'type': 'object', 'properties': {'message': {'type': 'string'}}}},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def change_password(self, request: Request) -> Response:
|
|
"""
|
|
Change user password.
|
|
"""
|
|
if not request.user.is_authenticated:
|
|
return Response(
|
|
{'error': 'Authentication required'},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
user = request.user
|
|
current_password = serializer.validated_data['current_password']
|
|
new_password = serializer.validated_data['new_password']
|
|
|
|
if not user.check_password(current_password):
|
|
return Response(
|
|
{'error': 'Current password is incorrect'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
user.set_password(new_password)
|
|
user.save(update_fields=['password'])
|
|
|
|
# Blacklist all existing tokens
|
|
jwt_service.blacklist_token('', blacklist_all_sessions=True)
|
|
|
|
logger.info(f"Password changed for user {user.id}")
|
|
|
|
return Response({'message': 'Password changed successfully'}, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Password change error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Password change failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="Reset Password",
|
|
description="Request password reset",
|
|
responses={200: {'type': 'object', 'properties': {'message': {'type': 'string'}}}},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def reset_password(self, request: Request) -> Response:
|
|
"""
|
|
Request password reset email.
|
|
"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
email = serializer.validated_data['email']
|
|
|
|
try:
|
|
user = User.objects.get(email__iexact=email)
|
|
except User.DoesNotExist:
|
|
# Don't reveal whether email exists
|
|
return Response({'message': 'Password reset email sent if email exists'})
|
|
|
|
# Generate password reset token
|
|
token = secrets.token_urlsafe(32)
|
|
reset_key = f"password_reset:{token}"
|
|
cache.set(reset_key, str(user.id), timeout=3600) # 1 hour
|
|
|
|
# Send password reset email
|
|
# This would integrate with email service
|
|
logger.info(f"Password reset requested for user {user.id}")
|
|
|
|
return Response({'message': 'Password reset email sent if email exists'})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Password reset request error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Password reset request failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="Confirm Password Reset",
|
|
description="Confirm password reset with token",
|
|
responses={200: {'type': 'object', 'properties': {'message': {'type': 'string'}}}},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def confirm_password_reset(self, request: Request) -> Response:
|
|
"""
|
|
Confirm password reset with token.
|
|
"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
token = serializer.validated_data['token']
|
|
new_password = serializer.validated_data['new_password']
|
|
|
|
# Verify token
|
|
reset_key = f"password_reset:{token}"
|
|
user_id = cache.get(reset_key)
|
|
|
|
if not user_id:
|
|
return Response(
|
|
{'error': 'Invalid or expired token'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
except User.DoesNotExist:
|
|
return Response(
|
|
{'error': 'User not found'},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
# Update password
|
|
user.set_password(new_password)
|
|
user.save(update_fields=['password'])
|
|
|
|
# Clear reset token
|
|
cache.delete(reset_key)
|
|
|
|
# Blacklist all existing tokens
|
|
jwt_service.blacklist_token('', blacklist_all_sessions=True)
|
|
|
|
logger.info(f"Password reset completed for user {user.id}")
|
|
|
|
return Response({'message': 'Password reset successfully'}, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Password reset confirmation error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Password reset failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="Magic Link",
|
|
description="Generate magic link for email authentication",
|
|
responses={200: {'type': 'object', 'properties': {'token': {'type': 'string'}}}},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def magic_link(self, request: Request) -> Response:
|
|
"""
|
|
Generate magic link for email authentication.
|
|
"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
email = serializer.validated_data['email']
|
|
tenant = getattr(request, 'tenant', None)
|
|
|
|
token = auth_backend.generate_magic_link(email, tenant)
|
|
|
|
# Send magic link email
|
|
# This would integrate with email service
|
|
logger.info(f"Magic link generated for {email}")
|
|
|
|
return Response({'token': token}, status=status.HTTP_200_OK)
|
|
|
|
except AuthenticationError as e:
|
|
return Response(
|
|
{'error': str(e)},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Magic link generation error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Magic link generation failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="Social Authentication",
|
|
description="Authenticate with social providers",
|
|
responses={200: AuthStatusSerializer},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def social_auth(self, request: Request) -> Response:
|
|
"""
|
|
Authenticate with social providers (Google, Facebook).
|
|
"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
provider = serializer.validated_data['provider']
|
|
access_token = serializer.validated_data['access_token']
|
|
|
|
# Map provider to authentication method
|
|
method_map = {
|
|
'google': 'google',
|
|
'facebook': 'facebook',
|
|
}
|
|
|
|
method = method_map.get(provider)
|
|
if not method:
|
|
return Response(
|
|
{'error': f'Provider {provider} not supported'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Authenticate with social provider
|
|
credentials = {'access_token': access_token}
|
|
if 'id_token' in serializer.validated_data:
|
|
credentials['id_token'] = serializer.validated_data['id_token']
|
|
|
|
user = auth_backend.authenticate(request, method=method, **credentials)
|
|
|
|
if not user:
|
|
return Response(
|
|
{'error': 'Social authentication failed'},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
|
|
# Generate JWT tokens
|
|
tenant = getattr(request, 'tenant', None) or getattr(user, 'tenant', None)
|
|
device_info = {
|
|
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
|
'ip_address': request.META.get('REMOTE_ADDR', ''),
|
|
'device_type': self._get_device_type(request),
|
|
}
|
|
|
|
tokens = jwt_service.generate_token_pair(user, tenant, device_info)
|
|
|
|
# Update user last login
|
|
user.last_login = timezone.now()
|
|
user.save(update_fields=['last_login'])
|
|
|
|
logger.info(f"User {user.id} logged in via {provider}")
|
|
|
|
return Response({
|
|
'user': self._serialize_user(user),
|
|
'tokens': tokens,
|
|
'mfa_status': mfa_service.get_mfa_status(user),
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
except AuthenticationError as e:
|
|
return Response(
|
|
{'error': str(e)},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Social authentication error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Social authentication failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="Biometric Authentication",
|
|
description="Authenticate with biometric data",
|
|
responses={200: AuthStatusSerializer},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def biometric_auth(self, request: Request) -> Response:
|
|
"""
|
|
Authenticate with biometric data.
|
|
"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
try:
|
|
user_id = serializer.validated_data['user_id']
|
|
biometric_token = serializer.validated_data['biometric_token']
|
|
|
|
user = auth_backend.authenticate(
|
|
request,
|
|
method='biometric',
|
|
user_id=user_id,
|
|
biometric_token=biometric_token
|
|
)
|
|
|
|
if not user:
|
|
return Response(
|
|
{'error': 'Biometric authentication failed'},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
|
|
# Generate JWT tokens
|
|
tenant = getattr(request, 'tenant', None) or getattr(user, 'tenant', None)
|
|
device_info = {
|
|
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
|
'ip_address': request.META.get('REMOTE_ADDR', ''),
|
|
'device_type': self._get_device_type(request),
|
|
}
|
|
|
|
tokens = jwt_service.generate_token_pair(user, tenant, device_info)
|
|
|
|
# Update user last login
|
|
user.last_login = timezone.now()
|
|
user.save(update_fields=['last_login'])
|
|
|
|
logger.info(f"User {user.id} logged in via biometric authentication")
|
|
|
|
return Response({
|
|
'user': self._serialize_user(user),
|
|
'tokens': tokens,
|
|
'mfa_status': mfa_service.get_mfa_status(user),
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
except AuthenticationError as e:
|
|
return Response(
|
|
{'error': str(e)},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Biometric authentication error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Biometric authentication failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="MFA Status",
|
|
description="Get MFA status for current user",
|
|
responses={200: {'type': 'object'}},
|
|
)
|
|
@action(detail=False, methods=['get'])
|
|
def mfa_status(self, request: Request) -> Response:
|
|
"""
|
|
Get MFA status for current user.
|
|
"""
|
|
if not request.user.is_authenticated:
|
|
return Response(
|
|
{'error': 'Authentication required'},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
|
|
try:
|
|
mfa_status = mfa_service.get_mfa_status(request.user)
|
|
return Response(mfa_status, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"MFA status error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Failed to get MFA status'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="Backup Codes",
|
|
description="Generate new backup codes",
|
|
responses={200: BackupCodeSerializer},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def backup_codes(self, request: Request) -> Response:
|
|
"""
|
|
Generate new backup codes.
|
|
"""
|
|
if not request.user.is_authenticated:
|
|
return Response(
|
|
{'error': 'Authentication required'},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
|
|
try:
|
|
backup_codes = mfa_service.generate_backup_codes(request.user)
|
|
return Response({'backup_codes': backup_codes}, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Backup codes generation error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Failed to generate backup codes'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
@extend_schema(
|
|
summary="Disable MFA",
|
|
description="Disable MFA for current user",
|
|
responses={200: {'type': 'object', 'properties': {'message': {'type': 'string'}}}},
|
|
)
|
|
@action(detail=False, methods=['post'])
|
|
def disable_mfa(self, request: Request) -> Response:
|
|
"""
|
|
Disable MFA for current user.
|
|
"""
|
|
if not request.user.is_authenticated:
|
|
return Response(
|
|
{'error': 'Authentication required'},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
|
|
try:
|
|
if mfa_service.disable_mfa(request.user):
|
|
return Response({'message': 'MFA disabled successfully'}, status=status.HTTP_200_OK)
|
|
else:
|
|
return Response(
|
|
{'error': 'Failed to disable MFA'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"MFA disable error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Failed to disable MFA'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
# Helper methods
|
|
|
|
def _serialize_user(self, user: User) -> Dict[str, Any]:
|
|
"""Serialize user data for API response."""
|
|
return {
|
|
'id': str(user.id),
|
|
'email': user.email,
|
|
'first_name': user.first_name,
|
|
'last_name': user.last_name,
|
|
'role': user.role,
|
|
'is_active': user.is_active,
|
|
'email_verified': user.email_verified,
|
|
'phone_number': user.phone_number,
|
|
'phone_verified': user.phone_verified,
|
|
'malaysian_ic': user.malaysian_ic,
|
|
'tenant_id': str(user.tenant.id) if user.tenant else None,
|
|
'last_login': user.last_login,
|
|
'created_at': user.created_at,
|
|
'updated_at': user.updated_at,
|
|
}
|
|
|
|
def _get_device_type(self, request: Request) -> str:
|
|
"""Detect device type from user agent."""
|
|
user_agent = request.META.get('HTTP_USER_AGENT', '').lower()
|
|
|
|
if 'mobile' in user_agent:
|
|
return 'mobile'
|
|
elif 'tablet' in user_agent:
|
|
return 'tablet'
|
|
elif 'desktop' in user_agent:
|
|
return 'desktop'
|
|
else:
|
|
return 'unknown'
|
|
|
|
|
|
# View functions for specific endpoints
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
def auth_status(request: Request) -> Response:
|
|
"""
|
|
Get authentication status for current user.
|
|
"""
|
|
try:
|
|
user = request.user
|
|
return Response({
|
|
'authenticated': True,
|
|
'user': {
|
|
'id': str(user.id),
|
|
'email': user.email,
|
|
'first_name': user.first_name,
|
|
'last_name': user.last_name,
|
|
'role': user.role,
|
|
'tenant_id': str(user.tenant.id) if user.tenant else None,
|
|
},
|
|
'mfa_status': mfa_service.get_mfa_status(user),
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Auth status error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Failed to get auth status'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
|
|
@api_view(['POST'])
|
|
@permission_classes([AllowAny])
|
|
def verify_email(request: Request) -> Response:
|
|
"""
|
|
Verify email address with OTP code.
|
|
"""
|
|
try:
|
|
email = request.data.get('email')
|
|
code = request.data.get('code')
|
|
|
|
if not email or not code:
|
|
return Response(
|
|
{'error': 'Email and code are required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Verify email OTP
|
|
if not auth_backend.verify_registration_otp(email, email_otp=code):
|
|
return Response(
|
|
{'error': 'Invalid verification code'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Update user email verification status
|
|
try:
|
|
user = User.objects.get(email__iexact=email)
|
|
user.email_verified = True
|
|
user.save(update_fields=['email_verified'])
|
|
except User.DoesNotExist:
|
|
pass
|
|
|
|
return Response({'message': 'Email verified successfully'}, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Email verification error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Email verification failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
|
|
@api_view(['POST'])
|
|
@permission_classes([AllowAny])
|
|
def verify_phone(request: Request) -> Response:
|
|
"""
|
|
Verify phone number with OTP code.
|
|
"""
|
|
try:
|
|
phone_number = request.data.get('phone_number')
|
|
code = request.data.get('code')
|
|
|
|
if not phone_number or not code:
|
|
return Response(
|
|
{'error': 'Phone number and code are required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Verify phone OTP
|
|
if not auth_backend.verify_registration_otp('', phone_otp=code, phone_number=phone_number):
|
|
return Response(
|
|
{'error': 'Invalid verification code'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Update user phone verification status
|
|
try:
|
|
user = User.objects.get(phone_number=phone_number)
|
|
user.phone_verified = True
|
|
user.save(update_fields=['phone_verified'])
|
|
except User.DoesNotExist:
|
|
pass
|
|
|
|
return Response({'message': 'Phone number verified successfully'}, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Phone verification error: {str(e)}")
|
|
return Response(
|
|
{'error': 'Phone verification failed'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
) |