Skip to content

Backends

The backends of Esmerald Simple JWT are what makes the Esmerald Simple JWT working properly.

A backend is what processes the information that will originate and create the tokens for the views of the package.

There are two required backends needed for the SimpleJWT to work properly.

These backends must be implemented at your own needs since this package is database agnostic. For example, it can be used with Edgy, Saffier, Mongoz or any other database at your choice.

Esmerald Simple JWT when it comes to the backend_refresh, already provides one almost generic that can be used almost out of the box if you use the default Token from the package but this can also be designed at your choice and you are not forced to use it.

Backend Authentication

When implementing an authentication backend, the authenticate() function must be implemented. The package does not force you to use what is provided but it does force you to implement the method or else an exception is raised.

To make your life easier, the package already provides a base class that you can and you should use it when designing your custom backend authentication.

from esmerald_simple_jwt.backends import BaseBackendAuthentication

This allows you to simple inherit the functionality and focus solely on the logic.

from typing import Any, Dict, Union

from esmerald_simple_jwt.backends import BaseBackendAuthentication


class MyBackendAuth(BaseBackendAuthentication):
    async def authenticate(self) -> Union[Dict[str, str], Any]:
        # Your authentication logic here.

BackendEmailAuthentication

This backend authentication only provides initial parameters to validate an email and password when those are provided via /signin (the default url) view.

This does not provide any additional logic but assumes the backend has two mandatory fields when instantiated.

from typing import Any, Dict, Union

from esmerald_simple_jwt.backends import BackendEmailAuthentication


class MyBackendAuth(BackendEmailAuthentication):
    async def authenticate(self) -> Union[Dict[str, str], Any]:
        email = self.email
        password = self.password

        # more logic to validate the email and password

BackendUsernameAuthentication

This backend authentication only provides initial parameters to validate an username and password when those are provided via /signin (the default url) view.

This does not provide any additional logic but assumes the backend has two mandatory fields when instantiated.

from typing import Any, Dict, Union

from esmerald_simple_jwt.backends import BackendUsernameAuthentication


class MyBackendAuth(BackendUsernameAuthentication):
    async def authenticate(self) -> Union[Dict[str, str], Any]:
        username = self.username
        password = self.password

        # more logic to validate the email and password

Examples

In the first page of the documentation, an example of an implementation of a backend was provided using the BackendEmailAuthentication.

Using the Edgy contrib from Esmerald, we were able to design our own backend authentication needed for the login of the app and return a dictionary containing the access_token and refresh_token.

myapp/apps/accounts/backends.py
from datetime import datetime

from edgy.exceptions import ObjectNotFound
from esmerald.conf import settings
from esmerald.exceptions import NotAuthorized
from esmerald.utils.module_loading import import_string

from esmerald_simple_jwt.backends import BackendEmailAuthentication as SimpleBackend
from esmerald_simple_jwt.schemas import TokenAccess
from esmerald_simple_jwt.token import Token

User = import_string("accounts.models.User")


class BackendAuthentication(SimpleBackend):
    """
    Using the `BackendEmailAuthentication` allows to inherit
    and use the `email` and `password` fields directly.
    """

    async def authenticate(self) -> str:
        """Authenticates a user and returns a JWT string"""
        try:
            user: User = await User.query.get(email=self.email)
        except ObjectNotFound:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a nonexistent user.
            await User().set_password(self.password)
        else:
            is_password_valid = await user.check_password(self.password)
            if is_password_valid and self.user_can_authenticate(user):
                # The lifetime of a token should be short, let us make 5 minutes.
                # You can use also the access_token_lifetime from the JWT config directly
                access_time = datetime.now() + settings.simple_jwt.access_token_lifetime
                refresh_time = datetime.now() + settings.simple_jwt.refresh_token_lifetime
                access_token = TokenAccess(
                    # The `token_type` defaults to `access_token`
                    access_token=self.generate_user_token(
                        user,
                        time=access_time,
                        token_type=settings.simple_jwt.access_token_name,
                    ),
                    # The `token_type` defaults to `refresh_token`
                    refresh_token=self.generate_user_token(
                        user,
                        time=refresh_time,
                        token_type=settings.simple_jwt.refresh_token_name,
                    ),
                )
                return access_token.model_dump()
            else:
                raise NotAuthorized(detail="Invalid credentials.")

    def user_can_authenticate(self, user):
        """
        Reject users with is_active=False. Custom user models that don't have
        that attribute are allowed.
        """
        return getattr(user, "is_active", True)

    def generate_user_token(self, user: User, token_type: str, time: datetime = None):
        """
        Generates the JWT token for the authenticated user.
        """
        if not time:
            later = datetime.now() + settings.simple_jwt.access_token_lifetime
        else:
            later = time

        token = Token(sub=str(user.id), exp=later)
        return token.encode(
            key=settings.simple_jwt.signing_key,
            algorithm=settings.simple_jwt.algorithm,
            token_type=token_type,
        )

Backend Refresh

When implementing a refresh backend, the refresh() function must be implemented. The package does not force you to use what is provided but it does force you to implement the method or else an exception is raised.

To make your life easier, the package already provides a base class that you can and you should use it when designing your custom backend authentication.

from esmerald_simple_jwt.backends import BaseRefreshAuthentication

This allows you to simple inherit the functionality and focus solely on the logic.

from typing import Any, Dict, Union

from esmerald_simple_jwt.backends import BaseRefreshAuthentication


class MyRefreshAuth(BaseRefreshAuthentication):
    async def refresh(self) -> Union[Dict[str, str], Any]:
        # Your authentication logic here.

If you are not interested in a lot of custom from your side and want to simple use the default Token when using the authentication backend, you can use automatically the provided RefreshAuthentication from the package.

from esmerald_simple_jwt.backends import RefreshAuthentication

Examples

In the first page of the documentation, an example of an implementation of a backend was provided using the RefreshAuthentication.

from datetime import datetime

from esmerald.conf import settings
from esmerald.exceptions import AuthenticationError, NotAuthorized
from jose import JWSError, JWTError

from esmerald_simple_jwt.backends import BaseRefreshAuthentication
from esmerald_simple_jwt.schemas import AccessToken, RefreshToken
from esmerald_simple_jwt.token import Token


class RefreshAuthentication(BaseRefreshAuthentication):
    """
    Refreshes the access token given a refresh token of a given user.

    This object does not perform any DB action, instead, uses the existing refresh
    token to generate a new access.
    """

    token: RefreshToken

    async def refresh(self) -> AccessToken:
        token = self.token.refresh_token

        try:
            token = Token.decode(
                token=token,
                key=settings.simple_jwt.signing_key,
                algorithms=[settings.simple_jwt.algorithm],
            )
        except (JWSError, JWTError) as e:
            raise AuthenticationError(str(e)) from e

        if token.token_type != settings.simple_jwt.refresh_token_name:
            raise NotAuthorized(detail="Only refresh tokens are allowed.")

        # Apply the maximum living time
        expiry_date = datetime.now() + settings.simple_jwt.access_token_lifetime

        # New token object
        new_token = Token(sub=token.sub, exp=expiry_date)

        # Encode the token
        access_token = new_token.encode(
            key=settings.simple_jwt.signing_key,
            algorithm=settings.simple_jwt.algorithm,
            token_type=settings.simple_jwt.access_token_name,
        )

        return AccessToken(access_token=access_token)

Using the SimpleJWT config

With the authentication and refresh backends built and designed, you can now simply add them to your SimpleJWT configuration.