authentik/authentik/stages/authenticator_static/models.py

134 lines
4.0 KiB
Python

"""Static Authenticator models"""
from base64 import b32encode
from os import urandom
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
from authentik.lib.models import SerializerModel
from authentik.stages.authenticator.models import Device, ThrottlingMixin
class AuthenticatorStaticStage(ConfigurableStage, FriendlyNamedStage, Stage):
"""Generate static tokens for the user as a backup."""
token_count = models.PositiveIntegerField(default=6)
token_length = models.PositiveIntegerField(default=12)
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.stages.authenticator_static.api import AuthenticatorStaticStageSerializer
return AuthenticatorStaticStageSerializer
@property
def view(self) -> type[View]:
from authentik.stages.authenticator_static.stage import AuthenticatorStaticStageView
return AuthenticatorStaticStageView
@property
def component(self) -> str:
return "ak-stage-authenticator-static-form"
def ui_user_settings(self) -> UserSettingSerializer | None:
return UserSettingSerializer(
data={
"title": self.friendly_name or str(self._meta.verbose_name),
"component": "ak-user-settings-authenticator-static",
}
)
def __str__(self) -> str:
return f"Static Authenticator Setup Stage {self.name}"
class Meta:
verbose_name = _("Static Authenticator Setup Stage")
verbose_name_plural = _("Static Authenticator Setup Stages")
class StaticDevice(SerializerModel, ThrottlingMixin, Device):
"""
A static :class:`~authentik.stages.authenticator.models.Device` simply consists of random
tokens shared by the database and the user.
These are frequently used as emergency tokens in case a user's normal
device is lost or unavailable. They can be consumed in any order; each
token will be removed from the database as soon as it is used.
This model has no fields of its own, but serves as a container for
:class:`StaticToken` objects.
.. attribute:: token_set
The RelatedManager for our tokens.
"""
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.stages.authenticator_static.api import StaticDeviceSerializer
return StaticDeviceSerializer
def get_throttle_factor(self):
return getattr(settings, "OTP_STATIC_THROTTLE_FACTOR", 1)
def verify_token(self, token):
verify_allowed, _ = self.verify_is_allowed()
if verify_allowed:
match = self.token_set.filter(token=token).first()
if match is not None:
match.delete()
self.throttle_reset()
else:
self.throttle_increment()
else:
match = None
return match is not None
class Meta(Device.Meta):
verbose_name = _("Static Device")
verbose_name_plural = _("Static Devices")
class StaticToken(models.Model):
"""
A single token belonging to a :class:`StaticDevice`.
.. attribute:: device
*ForeignKey*: A foreign key to :class:`StaticDevice`.
.. attribute:: token
*CharField*: A random string up to 16 characters.
"""
device = models.ForeignKey(StaticDevice, related_name="token_set", on_delete=models.CASCADE)
token = models.CharField(max_length=16, db_index=True)
class Meta:
verbose_name = _("Static Token")
verbose_name_plural = _("Static Tokens")
def __str__(self) -> str:
return "Static Token"
@staticmethod
def random_token():
"""
Returns a new random string that can be used as a static token.
:rtype: bytes
"""
return b32encode(urandom(5)).decode("utf-8").lower()