Models

Otp

Model that allows users to store and validate two-factor authentication credentials using the OTP method.

class Otp(TimeStampedModel):
     user = models.OneToOneField(User, on_delete=models.CASCADE)
     secret = models.CharField(max_length=255)
     words = models.TextField(blank=True, null=True)
     last_used_otp = models.CharField(max_length=7)
     last_otp_timestamp = models.DateTimeField(blank=True, null=True)

     @classmethod
     def verify_activation(cls, secret, otp):
         totp = pyotp.TOTP(secret)
         is_valid = True if totp.now() == otp else False
         return is_valid

     @classmethod
     def generate_random_config(cls, email):
         secret = pyotp.random_base32()
         provisioning_uri = pyotp.totp.TOTP(secret).provisioning_uri(
             name=email, issuer_name=app_settings.OTP_APP_DISPLAY_NAME
         )
         return {"secret": secret, "provisioning_uri": provisioning_uri}

     @classmethod
     def generate_config(cls, email, secret):
         provisioning_uri = pyotp.totp.TOTP(secret).provisioning_uri(
             name=email, issuer_name=app_settings.OTP_APP_DISPLAY_NAME
         )
         return {"secret": secret, "provisioning_uri": provisioning_uri}

     @classmethod
     def create(cls, email, secret):
         UserModel = User
         user = UserModel.objects.get(email=email)

         # Encrypt Secret before storing it in the database
         key = bytes(app_settings.OTP_SECRET_KEY, "utf-8")
         f = Fernet(key)
         secret = bytes(secret, "utf-8")
         secret_token = f.encrypt(secret)

         otp_config = cls()
         otp_config.secret = secret_token.decode("utf-8")
         otp_config.user = user
         otp_config.save()
         return otp_config

     def verify_otp(self, otp):
         # Decrypt Secret before using it
         key = bytes(app_settings.OTP_SECRET_KEY, "utf-8")
         f = Fernet(key)
         secret = f.decrypt(bytes(self.secret, "utf-8"))

         totp = pyotp.TOTP(secret)
         is_valid = True if totp.now() == otp else False
         replay_attempt = self.verify_replay_attack(otp=otp)

         if is_valid and not replay_attempt:
             self.last_used_otp = otp
             self.last_otp_timestamp = timezone.now()
             self.save()

         return is_valid and not replay_attempt

     def verify_replay_attack(self, otp):
         # To mitigate replay attacks we don't allow to reuse the same otp for a window of time after using it
         replay_attempt = False

         if otp == self.last_used_otp:
             now = timezone.now()
             time_window = now - self.last_otp_timestamp

             if time_window.seconds < 60:
                 replay_attempt = True

         return replay_attempt

     @classmethod
     def save_mnemonic_words(cls, email, words):
         key = bytes(app_settings.OTP_SECRET_KEY, "utf-8")
         f = Fernet(key)
         words = bytes(words.replace(" ", ""), "utf-8")
         words_token = f.encrypt(words)

         otp = Otp.objects.get(user__email=email)
         otp.words = words_token.decode("utf-8")
         otp.save()

         return otp

     def verify_mnemonic_words(self, words):
         key = bytes(app_settings.OTP_SECRET_KEY, "utf-8")
         f = Fernet(key)
         otp_words = f.decrypt(bytes(self.words, "utf-8")).decode("utf-8")

         is_valid = True if otp_words == words.replace(" ", "") else False

         return is_valid

The main fields are:

  • user: Relationship with the CustomUser model.

  • secret: Stores the value of a random secret generated in the generate_random_config method.

  • words: Field that stores the user’s recovery words.

  • last_used_otp: Field that records the last otp code used to avoid the replay attack.

  • last_otp_timestamp: Field that records the timestamp of the last code entered, this is used to avoid the replay attack.

The methods of this model are:

  • verify_activation: Method used to activate the OTP method in a user. For this you must enter the secret generated for the user and the otp code. This method returns a boolean value that depends on the verification and validation of the entered code. It does not require to instantiate the model.

  • generate_random_config: Method to generate the initial configuration of the OTP for the user. It does not require to instantiate the model.

  • generate_config: Method to obtain the configuration based on the user’s secret and email. It does not require to instantiate the model.

  • create: Method to register the OTP configuration of a user in the model. It does not require to instantiate the model.

  • verify_otp: Method to verify the otp code. Requires instantiating the model.

  • verify_replay_attack: Method that validates that an otp code is not used twice in the life time of the code. Requires instantiating the model.

  • save_mnemonic_words: Method that stores the recovery words in case the user loses the otp configuration information. It does not require to instantiate the model.

  • verify_mnemonic_words: Validates the user’s recovery words. Requires instantiating the model.