Source code for request.authentication.base
"""Provides base authentication class using the Composite pattern for modular authentication."""
from abc import ABC, abstractmethod
from typing import Never
from devices.models import (
DeviceModel,
)
from pki.models import IssuedCredentialModel
from pki.util.idevid import IDevIDAuthenticationError, IDevIDAuthenticator
from request.request_context import BaseRequestContext, HttpBaseRequestContext
from trustpoint.logger import LoggerMixin
[docs]
class AuthenticationComponent(ABC):
"""Abstract base class for authentication components."""
@abstractmethod
[docs]
def authenticate(self, context: BaseRequestContext) -> DeviceModel | None:
"""Authenticate a request using specific logic."""
[docs]
class ClientCertificateAuthentication(AuthenticationComponent, LoggerMixin):
"""Handles authentication via client certificates."""
[docs]
domain_credential_only: bool = True
def __init__(self, *, domain_credential_only: bool = True) -> None:
"""Initialize the client certificate authentication component."""
self.domain_credential_only = domain_credential_only
[docs]
def authenticate(self, context: BaseRequestContext) -> None:
"""Authenticate using the client certificate from the context."""
if not context.client_certificate:
return
client_certificate = context.client_certificate
try:
issued_credential = IssuedCredentialModel.get_credential_for_certificate(client_certificate)
if self.domain_credential_only:
is_valid, reason = issued_credential.is_valid_domain_credential()
else:
is_valid, reason = issued_credential.credential.is_valid_issued_credential()
if not is_valid:
self.logger.warning('Invalid client certificate: %s', reason)
error_message = f'Invalid HTTP_SSL_CLIENT_CERT header: {reason}'
self._raise_certificate_error(error_message)
self.logger.info('Successfully authenticated device via client certificate')
context.device = issued_credential.device
except IssuedCredentialModel.DoesNotExist:
self.logger.warning('Client certificate not found in issued credentials')
error_message = 'Client certificate not recognized'
self._raise_certificate_error(error_message)
except ValueError:
raise
except Exception as e:
self.logger.exception('Certificate authentication error')
error_message = 'Certificate authentication failed'
self._raise_certificate_error(error_message, e)
def _raise_certificate_error(self, message: str, cause: Exception | None = None) -> Never:
"""Raise certificate authentication error with proper exception chaining."""
if cause:
raise ValueError(message) from cause
raise ValueError(message)
[docs]
class IDevIDAuthentication(AuthenticationComponent, LoggerMixin):
"""Handles authentication via IDevID certificates."""
[docs]
def authenticate(self, context: BaseRequestContext) -> None:
"""Authenticate the request using the IDevID mechanism."""
# Early return if raw_message is missing
if not isinstance(context, HttpBaseRequestContext) or not context.raw_message:
return
try:
# domain can be None, in that case IDevIDAuthenticator will infer it from registration pattern
device_or_none = IDevIDAuthenticator.authenticate_idevid(context.raw_message, context.domain)
if device_or_none:
self.logger.info('Successfully authenticated device via IDevID')
context.device = device_or_none
if not context.domain:
device_domain = context.device.domain
if not device_domain:
error_message = 'IDevID authentication failed: Device domain is not set.'
self.logger.warning('IDevID authentication failed: Device domain is not set')
self._raise_idevid_error(error_message)
context.domain = device_domain
context.domain_str = device_domain.unique_name
else:
error_message = 'IDevID authentication failed: No device associated.'
self.logger.warning('IDevID authentication failed: No device associated')
self._raise_idevid_error(error_message)
except IDevIDAuthenticationError as e:
error_message = f'Error validating the IDevID: {e}'
self.logger.warning('Error validating the IDevID: %s', e)
raise ValueError(error_message) from e
except ValueError:
raise
except Exception as e:
error_message = 'IDevID authentication failed due to unexpected error'
self.logger.exception('Unexpected error during IDevID authentication')
raise ValueError(error_message) from e
def _raise_idevid_error(self, message: str) -> Never:
"""Raise IDevID authentication error."""
raise ValueError(message)
[docs]
class CompositeAuthentication(AuthenticationComponent, LoggerMixin):
"""Composite authenticator for grouping and executing multiple authentication methods."""
def __init__(self) -> None:
"""Initialize the composite authenticator with a set of authentication components."""
[docs]
self.components: list[AuthenticationComponent] = []
[docs]
def add(self, component: AuthenticationComponent) -> None:
"""Add an authentication component to the composite."""
self.components.append(component)
[docs]
def remove(self, component: AuthenticationComponent) -> None:
"""Remove an authentication component from the composite."""
self.components.remove(component)
[docs]
def authenticate(self, context: BaseRequestContext) -> None:
"""Authenticate the request using all registered components."""
authentication_errors = []
self.logger.debug('Authentication components to try: %s',
[component.__class__.__name__ for component in self.components])
for component in self.components:
try:
component.authenticate(context)
if context.device is not None:
self.logger.info('Authentication successful using %s', component.__class__.__name__)
return
except ValueError as e:
authentication_errors.append(f'{component.__class__.__name__}: {e}')
continue
except Exception:
self.logger.exception('Unexpected error in %s', component.__class__.__name__)
authentication_errors.append(f'{component.__class__.__name__}: Unexpected error')
continue
error_message = 'Authentication failed: All authentication methods were unsuccessful.'
self.logger.warning('Authentication failed for all methods: %s', authentication_errors)
context.http_response_content = 'Authentication failed.'
context.http_response_status = 403
raise ValueError(error_message)