Source code for deux.serializers
from __future__ import absolute_import, unicode_literals
import six
from rest_framework import serializers
from deux.app_settings import mfa_settings
from deux import strings
from deux.constants import SMS
from deux.exceptions import FailedChallengeError
from deux.services import MultiFactorChallenge, verify_mfa_code
[docs]class MultiFactorAuthSerializer(serializers.ModelSerializer):
"""
class::MultiFactorAuthSerializer()
Basic MultiFactorAuthSerializer that encodes MFA objects into a standard
response.
The standard response returns whether MFA is enabled, the challenge
type, and the user's phone number.
"""
[docs] def to_representation(self, mfa_instance):
"""
Encodes an MFA instance as the standard response.
:param mfa_instance: :class:`MultiFactorAuth` instance to use.
:returns: Dictionary with ``enabled``, ``challengetype``, and
``phone_number`` from the MFA instance.
"""
data = {"enabled": mfa_instance.enabled}
if mfa_instance.enabled:
data["challenge_type"] = mfa_instance.challenge_type
if mfa_instance.phone_number:
data["phone_number"] = mfa_instance.phone_number
return data
class Meta:
model = mfa_settings.MFA_MODEL
[docs]class _BaseChallengeRequestSerializer(MultiFactorAuthSerializer):
"""
class::_BaseChallengeRequestSerializer()
Base Serializer class for all challenge request.
"""
@property
def challenge_type(self):
"""
Represents the challenge type this serializer represents.
:raises NotImplemented: If the extending class does not define
``challenge_type``.
"""
raise NotImplemented # pragma: no cover
[docs] def execute_challenge(self, instance):
"""
Execute challenge for this instance based on the ``challenge_type``.
:param instance: :class:`MultiFactorAuth` instance to use.
:raises serializers.ValidationError: If the challenge fails to execute.
"""
try:
MultiFactorChallenge(
instance, self.challenge_type).generate_challenge()
except FailedChallengeError as e:
raise serializers.ValidationError({
"detail": six.text_type(e)
})
[docs] def validate(self, internal_data):
"""
Validate the request to enable MFA through this challenge.
Extending classes should extend for additional functionality. The
base functionality ensures that MFA is not already enabled.
:param internal_data: Dictionary of the request data.
:raises serializers.ValidationError: If MFA is already enabled.
"""
if self.instance.enabled:
raise serializers.ValidationError({
"detail": strings.ENABLED_ERROR
})
return internal_data
[docs] def update(self, mfa_instance, validated_data):
"""
If the request is valid, the serializer calls update which executes
the ``challenge_type``.
:param mfa_instance: :class:`MultiFactorAuth` instance to use.
:param validated_data: Data returned by ``validate``.
"""
self.execute_challenge(mfa_instance)
[docs]class _BaseChallengeVerifySerializer(MultiFactorAuthSerializer):
"""
class::_BaseChallengeVerifySerializer()
This serializer first extracts MFA code from request body
(`to_internal_value`). It then verifies the code against the
corresponding `MultiFactorAuth` instance (`validate`). If the code
is valid, it enables MFA based on the challenge type (`update`).
"""
#: Requests to verify an MFA code must include an ``mfa_code``.
mfa_code = serializers.CharField()
@property
def challenge_type(self):
"""
Represents the challenge type this serializer represents.
:raises NotImplemented: If the extending class does not define
``challenge_type``.
"""
raise NotImplemented # pragma: no cover
[docs] def validate(self, internal_data):
"""
Validates the request to verify the MFA code. It first ensures that
MFA is not already enabled and then verifies that the MFA code is the
correct code.
:param internal_data: Dictionary of the request data.
:raises serializers.ValidationError: If MFA is already enabled or if
the inputted MFA code is not valid.
"""
if self.instance.enabled:
raise serializers.ValidationError({
"detail": strings.ENABLED_ERROR
})
mfa_code = internal_data.get("mfa_code")
bin_key = self.instance.get_bin_key(self.challenge_type)
if not verify_mfa_code(bin_key, mfa_code):
raise serializers.ValidationError({
"mfa_code": strings.INVALID_MFA_CODE_ERROR
})
return {"mfa_code": mfa_code}
[docs] def update(self, mfa_instance, validated_data):
"""
If the request is valid, the serializer enables MFA on this instance
for this serializer's ``challenge_type``.
:param mfa_instance: :class:`MultiFactorAuth` instance to use.
:param validated_data: Data returned by ``validate``.
"""
mfa_instance.enable(self.challenge_type)
return mfa_instance
class Meta(MultiFactorAuthSerializer.Meta):
fields = ("mfa_code",)
[docs]class SMSChallengeRequestSerializer(_BaseChallengeRequestSerializer):
"""
class::SMSChallengeRequestSerializer()
Serializer that facilitates a request to enable MFA over SMS.
"""
#: This serializer represents the ``SMS`` challenge type.
challenge_type = SMS
[docs] def update(self, mfa_instance, validated_data):
"""
If the request data is valid, the serializer executes the challenge
by calling the super method and also saves the phone number the user
requested the SMS to.
:param mfa_instance: :class:`MultiFactorAuth` instance to use.
:param validated_data: Data returned by ``validate``.
"""
mfa_instance.phone_number = validated_data["phone_number"]
super(SMSChallengeRequestSerializer, self).update(
mfa_instance, validated_data)
mfa_instance.save()
return mfa_instance
class Meta(_BaseChallengeRequestSerializer.Meta):
fields = ("phone_number",)
extra_kwargs = {
"phone_number": {
"required": True,
},
}
[docs]class SMSChallengeVerifySerializer(_BaseChallengeVerifySerializer):
"""
class::SMSChallengeVerifySerializer()
Extension of ``_BaseChallengeVerifySerializer`` that implements
challenge verification for the SMS challenge.
"""
#: This serializer represents the ``SMS`` challenge type.
challenge_type = SMS
[docs]class BackupCodeSerializer(serializers.ModelSerializer):
"""
class::BackupCodeSerializer()
Serializer for the user requesting backup codes.
"""
#: Serializer field for the backup code.
backup_code = serializers.SerializerMethodField()
[docs] def get_backup_code(self, instance):
"""
Method for retrieving the backup code. On every request, the backup
code is refreshed.
:param instance: :class:`MultiFactorAuth` instance to use.
:raises serializers.ValidationError: If MFA is disabled.
"""
if self.instance.enabled:
return self.instance.refresh_backup_code()
else:
raise serializers.ValidationError({
"backup_code": strings.DISABLED_ERROR
})
class Meta:
model = mfa_settings.MFA_MODEL
fields = ("backup_code",)