def validate_user(self, username, password, client, request, *args, **kwargs): """ Overrides the OAuth2Validator validate method to implement multi factor authentication. If MFA is disabled, authentication requires just a username and password. If MFA is enabled, authentication requires a username, password, and either a MFA code or a backup code. If the request only provides the username and password, the server will generate an appropriate challenge and respond with `mfa_required = True`. Upon using a backup code to authenticate, MFA will be disabled. :param attrs: Dictionary of data inputted by the user. :raises deux.oauth2.exceptions.InvalidLoginError: If invalid MFA code or backup code are submitted. Also if both types of code are submitted simultaneously. :raises deux.oauth2.exceptions.ChallengeRequiredMessage: If the user has MFA enabled but only supplies the correct username and password. This exception will prompt the OAuth2 system to send a response asking the user to supply an MFA code. """ user = authenticate(username=username, password=password) if not (user and user.is_active): raise InvalidLoginError( force_text(strings.INVALID_CREDENTIALS_ERROR)) mfa = None if hasattr(user, "multi_factor_auth"): mfa = user.multi_factor_auth if mfa and mfa.enabled: mfa_code = request.extra_credentials.get("mfa_code") backup_code = request.extra_credentials.get("backup_code") if mfa_code and backup_code: raise InvalidLoginError(force_text(strings.BOTH_CODES_ERROR)) elif mfa_code: bin_key = mfa.get_bin_key(mfa.challenge_type) if not verify_mfa_code(bin_key, mfa_code): raise InvalidLoginError( force_text(strings.INVALID_MFA_CODE_ERROR)) elif backup_code: if not mfa.check_and_use_backup_code(backup_code): raise InvalidLoginError( force_text(strings.INVALID_BACKUP_CODE_ERROR)) else: challenge = MultiFactorChallenge(mfa, mfa.challenge_type) challenge.generate_challenge() raise ChallengeRequiredMessage(mfa.challenge_type) request.user = user return True
def test_verify_mfa_code_fail(self): int_mfa_code = int(generate_mfa_code(self.bin_key, 0)) mfa_code_tests = (None, "", generate_mfa_code(self.bin_key, -3), generate_mfa_code(self.bin_key, -2), generate_mfa_code(self.bin_key, 2), generate_mfa_code(self.bin_key, 3), six.text_type(int_mfa_code + 1).zfill( mfa_settings.MFA_CODE_NUM_DIGITS), "abcdef") for mfa_code in mfa_code_tests: self.assertFalse(verify_mfa_code(self.bin_key, mfa_code))
def validate(self, attrs): """ Extends the AuthTokenSerializer validate method to implement multi factor authentication. If MFA is disabled, authentication requires just a username and password. If MFA is enabled, authentication requires a username, password, and either a MFA code or a backup code. If the request only provides the username and password, the server will generate an appropriate challenge and respond with `mfa_required = True`. Upon using a backup code to authenticate, MFA will be disabled. :param attrs: Dictionary of data inputted by the user. :raises serializers.ValidationError: If invalid MFA code or backup code are submitted. Also if both types of code are submitted simultaneously. """ attrs = super(MFAAuthTokenSerializer, self).validate(attrs) # User must exist if super method didn't throw error. user = attrs["user"] assert user is not None, "User should exist after super call." multi_factor_model_name = mfa_settings.MFA_MODEL._meta.model_name mfa = getattr(user, "{}_multi_factor_auth".format(multi_factor_model_name), None) if mfa and mfa.enabled: mfa_code = attrs.get("mfa_code") backup_code = attrs.get("backup_code") if mfa_code and backup_code: raise serializers.ValidationError( force_text(strings.BOTH_CODES_ERROR)) elif mfa_code: bin_key = mfa.get_bin_key(mfa.challenge_type) if not verify_mfa_code(bin_key, mfa_code): raise serializers.ValidationError( force_text(strings.INVALID_MFA_CODE_ERROR)) elif backup_code: if not mfa.check_and_use_backup_code(backup_code): raise serializers.ValidationError( force_text(strings.INVALID_BACKUP_CODE_ERROR)) else: challenge = MultiFactorChallenge(mfa, mfa.challenge_type) challenge.generate_challenge() attrs["mfa_required"] = True attrs["mfa_type"] = mfa.challenge_type return attrs
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}
def test_verify_mfa_code_success(self): mfa_code_tests = (generate_mfa_code(self.bin_key, -1), generate_mfa_code(self.bin_key, 0), generate_mfa_code(self.bin_key, 1)) for mfa_code in mfa_code_tests: self.assertTrue(verify_mfa_code(self.bin_key, mfa_code))
def verify_challenge_code(self, mfa_code): return verify_mfa_code(self.bin_key, mfa_code)