Source code for deux.abstract_models
from __future__ import absolute_import, unicode_literals
from binascii import unhexlify
from django.contrib.auth.models import User
from django.db import models
from django.utils.crypto import constant_time_compare
from deux.app_settings import mfa_settings
from deux.constants import CHALLENGE_TYPES, DISABLED, SMS
from deux.services import generate_key
from deux.validators import phone_number_validator
[docs]class AbstractMultiFactorAuth(models.Model):
"""
class::AbstractMultiFactorAuth()
This abstract class holds user information, MFA status, and secret
keys for the user.
"""
#: Different status options for this MFA object.
CHALLENGE_CHOICES = (
(SMS, "SMS"),
(DISABLED, "Off"),
)
#: User this MFA object represents.
user = models.OneToOneField(
User, related_name="multi_factor_auth", primary_key=True)
#: User's phone number.
phone_number = models.CharField(
max_length=15, default="", blank=True,
validators=[phone_number_validator])
#: Challenge type used for MFA.
challenge_type = models.CharField(
max_length=16, default=DISABLED,
blank=True, choices=CHALLENGE_CHOICES
)
#: Secret key used for backup code.
backup_key = models.CharField(
max_length=32, default="", blank=True,
help_text="Hex-Encoded Secret Key"
)
#: Secret key used for SMS codes.
sms_secret_key = models.CharField(
max_length=32, default=generate_key,
help_text="Hex-Encoded Secret Key"
)
@property
def sms_bin_key(self):
"""Returns binary data of the SMS secret key."""
return unhexlify(self.sms_secret_key)
@property
def enabled(self):
"""Returns if MFA is enabled."""
return self.challenge_type in CHALLENGE_TYPES
@property
def backup_code(self):
"""Returns the users backup code."""
return self.backup_key.upper()[:mfa_settings.BACKUP_CODE_DIGITS]
[docs] def get_bin_key(self, challenge_type):
"""
Returns the key associated with the inputted challenge type.
:param challenge_type: The challenge type the key is requested for.
The type must be in the supported
`CHALLENGE_TYPES`.
:raises AssertionError: If ``challenge_type`` is not a supported
challenge type.
"""
assert challenge_type in CHALLENGE_TYPES, (
"'{challenge}' is not a valid challenge type.".format(
challenge=challenge_type)
)
return {
SMS: self.sms_bin_key
}.get(challenge_type, None)
[docs] def enable(self, challenge_type):
"""
Enables MFA for this user with the inputted challenge type.
The enabling process includes setting this objects challenge type and
generating a new backup key.
:param challenge_type: Enable MFA for this type of challenge. The type
must be in the supported `CHALLENGE_TYPES`.
:raises AssertionError: If ``challenge_type`` is not a supported
challenge type.
"""
assert challenge_type in CHALLENGE_TYPES, (
"'{challenge}' is not a valid challenge type.".format(
challenge=challenge_type)
)
self.challenge_type = challenge_type
self.backup_key = generate_key()
self.save()
[docs] def disable(self):
"""
Disables MFA for this user.
The disabling process includes setting the objects challenge type to
`DISABLED`, and removing the `backup_key` and `phone_number`.
"""
self.challenge_type = DISABLED
self.backup_key = ""
self.phone_number = ""
self.save()
[docs] def refresh_backup_code(self):
"""
Refreshes the users backup key and returns a new backup code.
This method should be used to request new backup codes for the user.
"""
assert self.enabled, (
"MFA must be on to run refresh_backup_codes."
)
self.backup_key = generate_key()
self.save()
return self.backup_code
[docs] def check_and_use_backup_code(self, code):
"""
Checks if the inputted backup code is correct and disables MFA if
the code is correct.
This method should be used for authenticating with a backup code. Using
a backup code to authenticate disables MFA as a side effect.
"""
backup = self.backup_code
if code and constant_time_compare(code, backup):
self.disable()
return True
return False
class Meta:
abstract = True