Source code for crossauth_backend.auth

# Copyright (c) 2024 Matthew Baker.  All rights reserved.  Licenced under the Apache Licence 2.0.  See LICENSE file
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, TypedDict, Any
from crossauth_backend.common.error import CrossauthError, ErrorCode
from crossauth_backend.common.interfaces import Key, User, UserSecretsInputFields, UserInputFields

[docs] class AuthenticationParameters(UserSecretsInputFields, total=False): """ Parameters needed for this this class to authenticator a user (besides username) An example is `password` """ otp: str password: str
[docs] class AuthenticationOptions(TypedDict, total=False): """ Options to pass to the constructor. """ friendly_name: str """ If passed, this is what will be displayed to the user when selecting an authentication method. """
[docs] class AuthenticatorCapabilities(TypedDict, total=True): can_create_user: bool can_update_user: bool can_update_secrets: bool
[docs] class Authenticator(ABC): """ Base class for username/password authentication. Subclass this if you want something other than PBKDF2 password hashing. """ friendly_name: str factor_name: str = "" def __init__(self, options: AuthenticationOptions = {}): """ Constructor. :param AuthenticationOptions options: see :class:`AuthenticationOptions` """ if "friendly_name" not in options: raise CrossauthError(ErrorCode.Configuration, "Authenticator must have a friendly name") self.friendly_name = options["friendly_name"]
[docs] @abstractmethod def skip_email_verification_on_signup(self) -> bool: pass
[docs] @abstractmethod async def prepare_configuration(self, user: UserInputFields) -> Optional[Dict[str, Dict[str, Any]]]: pass
[docs] @abstractmethod async def reprepare_configuration(self, username: str, session_key: Key) -> Optional[Dict[str, Dict[str, Any] | Optional[Dict[str, Any]]]]: pass
[docs] @abstractmethod def mfa_type(self) -> str: pass
[docs] @abstractmethod def mfa_channel(self) -> str: pass
[docs] @abstractmethod async def authenticate_user(self, user: UserInputFields|None, secrets: UserSecretsInputFields, params: AuthenticationParameters) -> None: pass
[docs] @abstractmethod async def create_persistent_secrets(self, username: str, params: AuthenticationParameters, repeat_params: AuthenticationParameters|None = None) -> UserSecretsInputFields: pass
[docs] @abstractmethod async def create_one_time_secrets(self, user: User) -> UserSecretsInputFields: pass
[docs] @abstractmethod def can_create_user(self) -> bool: pass
[docs] @abstractmethod def can_update_secrets(self) -> bool: pass
[docs] @abstractmethod def can_update_user(self) -> bool: pass
[docs] @abstractmethod def secret_names(self) -> List[str]: pass
[docs] @abstractmethod def transient_secret_names(self) -> List[str]: pass
[docs] @abstractmethod def validate_secrets(self, params: AuthenticationParameters) -> List[str]: pass
[docs] def capabilities(self) -> AuthenticatorCapabilities: return AuthenticatorCapabilities( can_create_user=self.can_create_user(), can_update_user=self.can_update_user(), can_update_secrets=self.can_update_secrets() )
[docs] def require_user_entry(self) -> bool: """ If your authenticator doesn't need a user to be in the table (because it can create one), override this and return false. Default is true """ return True
[docs] class PasswordAuthenticator(Authenticator): """ base class for authenticators that validate passwords """
[docs] def secret_names(self) -> List[str]: return ["password"]
[docs] def transient_secret_names(self) -> List[str]: return []
[docs] def mfa_type(self) -> str: return "none"
[docs] def mfa_channel(self) -> str: return "none"