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.
- The backend_authentication
- The backend_refresh.
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
.
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.