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"