Source code for dmr.security.token.token

import abc
import datetime as dt
from typing import TYPE_CHECKING, Final, Generic, Self

from django.utils.crypto import salted_hmac
from django.views.decorators.debug import sensitive_variables
from typing_extensions import Sentinel, TypeVar

from dmr.security.token.constants import TOKEN_DEFAULT_EXPIRY
from dmr.types import EMPTY

if TYPE_CHECKING:
    from django.contrib.auth.base_user import AbstractBaseUser


class _TokenLikeBase:
    __slots__ = ()

    @property
    @abc.abstractmethod
    def is_expired(self) -> bool:
        """Is current token expired?"""
        raise NotImplementedError

    @property
    @abc.abstractmethod
    def is_active(self) -> bool:
        """Is current token active?"""
        raise NotImplementedError


_UserT = TypeVar('_UserT', bound='AbstractBaseUser', default='AbstractBaseUser')


[docs] class TokenLikeSync(_TokenLikeBase, Generic[_UserT]): """ Base sync interface for all token models. This type is generic on the user type. If you want to customize the ``User`` object that you are working with, just inherit from a generic version: ``TokenLikeSync[CustomUser]``. Otherwise, you would work with :class:`~django.contrib.auth.models.AbstractBaseUser`, which is the default type parameter. .. seealso:: :ref:`custom-user-token-model` for a complete example. .. versionadded:: 0.12.0 """ __slots__ = ()
[docs] @abc.abstractmethod def get_user(self) -> _UserT: """Get user that this token belongs to.""" raise NotImplementedError
[docs] @abc.abstractmethod def mark_used(self) -> None: """Mark this token as used.""" raise NotImplementedError
[docs] @abc.abstractmethod def revoke( self, *, at: dt.datetime | None = None, ) -> None: """Revoke this token.""" raise NotImplementedError
[docs] @classmethod @abc.abstractmethod def issue( # noqa: WPS211 cls, *, user: _UserT, name: str, expires_at: dt.datetime | Sentinel | None = EMPTY, token_size: int | None = None, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None, ) -> tuple[Self, str]: """ Create new token. Parameters: user: ``User`` instance to store. Type annotation can be customized via generic type arguments. name: Name of the token. expires_at: When this token expires? Can be one of three: a specific date, ``None`` for tokens that do not expire at all, ``EMPTY`` to calculate the exiry relative to the current date. token_size: Size of the raw token in chars. token_secret: Secret key to be used for the token hash. Defaults to ``settings.SECRET_KEY``. token_salt: Salt to be used for the token hash. token_algorithm: Salt to be used for the token hash. Defaults to ``sha256``. """ raise NotImplementedError
[docs] @classmethod @abc.abstractmethod def find_raw( cls, raw_token: str, *, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None, ) -> Self | None: """Find token by its hash.""" raise NotImplementedError
[docs] class TokenLikeAsync(_TokenLikeBase, Generic[_UserT]): """ Base async interface for all token models. This type is generic on the user type. If you want to customize the ``User`` object that you are working with, just inherit from a generic version: ``TokenLikeAsync[CustomUser]``. Otherwise, you would work with :class:`~django.contrib.auth.models.AbstractBaseUser`, which is the default type parameter. .. seealso:: :ref:`custom-user-token-model` for a complete example. .. versionadded:: 0.12.0 """ __slots__ = ()
[docs] @abc.abstractmethod async def aget_user(self) -> _UserT: """Async get user that this token belongs to.""" raise NotImplementedError
[docs] @abc.abstractmethod async def amark_used(self) -> None: """Async mark this token as used.""" raise NotImplementedError
[docs] @abc.abstractmethod async def arevoke( self, *, at: dt.datetime | None = None, ) -> None: """Async revoke this token.""" raise NotImplementedError
[docs] @classmethod @abc.abstractmethod async def aissue( # noqa: WPS211 cls, *, user: _UserT, name: str, expires_at: dt.datetime | Sentinel | None = EMPTY, token_size: int | None = None, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None, ) -> tuple[Self, str]: """Async version of :meth:`TokenLikeSync.issue`.""" raise NotImplementedError
[docs] @classmethod @abc.abstractmethod async def afind_raw( cls, raw_token: str, *, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None, ) -> Self | None: """Async find token by its hash.""" raise NotImplementedError
DEFAULT_TOKEN_SALT: Final = 'dmr.security.token' # noqa: S105 DEFAULT_TOKEN_ALGORITHM: Final = 'sha256' # noqa: S105
[docs] @sensitive_variables() def get_token_hash( raw_token: str, *, secret: str | None, salt: str | None = None, algorithm: str | None = None, ) -> str: """ Hash the token value with the secret key. Parameters: raw_token: Raw string to be hashed. secret: What secret should we use for token hash? Default to ``settings.SECRET_KEY``. salt: What salt should we use for token hash? algorithm: What algorithm should we use for token hash? .. versionadded:: 0.12.0 """ return salted_hmac( salt or DEFAULT_TOKEN_SALT, raw_token, secret=secret, algorithm=algorithm or DEFAULT_TOKEN_ALGORITHM, ).hexdigest()
[docs] def resolve_expiry( expires_at: dt.datetime | Sentinel | None, *, expiration: dt.timedelta | None = None, ) -> dt.datetime | None: """ Resolve expiery for optional value. .. versionadded:: 0.12.0 """ # TODO: fix after sentinels are fully supported if not isinstance(expires_at, Sentinel): return expires_at resolved_expiration = expiration or TOKEN_DEFAULT_EXPIRY return dt.datetime.now(dt.UTC) + resolved_expiration