Ejemplo n.º 1
0
 def test_bogus_user(self):
     """ A bogus user is denied """
     os.environ['common_name'] = 'user-who-does-not-exist'
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     res = library.main_authentication()
     self.assertFalse(res, 'invalid users must be denied')
Ejemplo n.º 2
0
 def test_bogus_user(self):
     """ A bogus user is denied """
     os.environ['common_name'] = 'user-who-does-not-exist'
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertFalse(res, 'invalid users must be denied')
Ejemplo n.º 3
0
 def test_1fa_user_bad_pw(self):
     """ A 1FA user with a bad password fails """
     if not self.one_fa_user:  # pragma: no cover
         return self.skipTest('No testing/one_fa_user defined')
     os.environ['common_name'] = self.one_fa_user
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     res = library.main_authentication()
     self.assertFalse(res, '1fa user with bad password must be denied')
Ejemplo n.º 4
0
 def test_2fa_user_good(self):
     """ A 2FA user with a bad push fails  PLEASE ALLOW """
     if not self.deep_test_main:  # pragma: no cover
         return self.skipTest('because of .deep_testing preference')
     os.environ['common_name'] = self.normal_user
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertTrue(res, '2fa user with an allow must be True')
Ejemplo n.º 5
0
 def test_2fa_user_bad(self):
     """ A 2FA user with a bad push fails  PLEASE DENY """
     if not self.deep_test_main:
         return self.skipTest('because of .deep_testing preference')
     os.environ['common_name'] = self.normal_user
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertFalse(res, '2fa user with a deny must be False')
Ejemplo n.º 6
0
 def test_2fa_user_good(self):
     """ A 2FA user with a bad push fails  PLEASE ALLOW """
     if not self.deep_test_main:  # pragma: no cover
         return self.skipTest('because of .deep_testing preference')
     os.environ['common_name'] = self.normal_user
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertTrue(res, '2fa user with an allow must be True')
Ejemplo n.º 7
0
 def test_1fa_user_attempts_2fa(self):
     """ A 1FA user trying to 2FA fails """
     # This is a weird test that stems from a 1FA user pretending to
     # have a Duo.
     if not self.one_fa_user:  # pragma: no cover
         return self.skipTest('No testing/one_fa_user defined')
     os.environ['common_name'] = self.one_fa_user
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     res = library.main_authentication()
     self.assertFalse(res, '1fa user attempting to 2fa must be denied')
Ejemplo n.º 8
0
 def test_1fa_user_good_pw(self):
     """ A 1FA user with a good password works """
     if not self.one_fa_user:  # pragma: no cover
         return self.skipTest('No testing/one_fa_user defined')
     if not self.one_fa_pass:  # pragma: no cover
         return self.skipTest('No testing/one_fa_pass defined')
     os.environ['common_name'] = self.one_fa_user
     os.environ['password'] = self.one_fa_pass
     library = DuoOpenVPN()
     res = library.main_authentication()
     self.assertTrue(res, '1fa user with good password gets accepted')
Ejemplo n.º 9
0
 def test_1fa_user_bad_pw(self):
     """ A 1FA user with a bad password fails """
     try:
         one_fa_user = self.main_object.configfile.get(
             'testing', 'one_fa_user')
     except (NoOptionError, NoSectionError):  # pragma: no cover
         return self.skipTest('No testing/one_fa_user defined')
     os.environ['common_name'] = one_fa_user
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertFalse(res, '1fa user with bad password must be denied')
Ejemplo n.º 10
0
 def test_1fa_user_bad_pw(self):
     """ A 1FA user with a bad password fails """
     try:
         one_fa_user = self.main_object.configfile.get('testing',
                                                       'one_fa_user')
     except (NoOptionError, NoSectionError):  # pragma: no cover
         return self.skipTest('No testing/one_fa_user defined')
     os.environ['common_name'] = one_fa_user
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertFalse(res, '1fa user with bad password must be denied')
Ejemplo n.º 11
0
 def test_1fa_user_bad_pw(self):
     """ A 1FA user with a bad password fails """
     if not self.main_object.configfile.has_section('testing'):
         return self.skipTest('No testing section defined')
     if not self.main_object.configfile.has_option('testing',
                                                   'one_fa_user'):
         return self.skipTest('No testing/one_fa_user defined')
     one_fa_user = self.main_object.configfile.get('testing', 'one_fa_user')
     os.environ['common_name'] = one_fa_user
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertFalse(res, '1fa user with bad password must be denied')
Ejemplo n.º 12
0
def main():
    """
        The main function.  Handles file-writing back to openvpn.
        All the good stuff is in class DuoOpenVPN
    """
    #
    # 'auth_control_file' is passed to this script via an
    # environmental variable.  It's an ephemeral file.
    # If we allow a user, put a 1 in the file.
    # If we deny a user, put a 0 in the file.
    #
    control_file_path = os.environ.get('auth_control_file')
    if control_file_path is None:
        # Not having this set is bad: we have no way to tell openvpn
        # what's happened.  Or, you're running the script by hand.
        # Either way, there's no point in continuing.  Get out.
        # We print to STDOUT because this is likely a human, instead
        # of an actual run.  It's possible that we should 'log' this.
        print('No auth_control_file env variable provided.')
        sys.exit(1)
    # There are many more environmental variables needed for this
    # whole process to work.  They are captured/realized farther down in
    # the stack, so, if this looks sad and short, it's intentional.
    # The env-variable work is in OpenVPNCredentials

    auth_object = DuoOpenVPN()
    try:
        if auth_object.duo_timeout:
            signal.signal(signal.SIGALRM, duo_timeout_handler)
            signal.alarm(auth_object.duo_timeout)
        should_allow_in = auth_object.main_authentication()
        if auth_object.duo_timeout:
            signal.alarm(0)
    except DuoTimeoutError:
        should_allow_in = auth_object.failopen
    if should_allow_in:
        writeout_value = str(1)
    else:
        writeout_value = str(0)

    try:
        with open(control_file_path, 'w') as filehandle:
            filehandle.write(writeout_value)
    except IOError:
        # I couldn't write to the file, so we can't tell openvpn what
        # happened.  There's nothing to do but error out.
        sys.exit(1)

    # we wrote out in the try, so we're done.
    sys.exit(0)
Ejemplo n.º 13
0
 def test_1fa_user_attempts_2fa(self):
     """ A 1FA user trying to 2FA fails """
     # This is a weird test that stems from a 1FA user pretending to
     # have a Duo.
     try:
         one_fa_user = self.main_object.configfile.get(
             'testing', 'one_fa_user')
     except (NoOptionError, NoSectionError):  # pragma: no cover
         return self.skipTest('No testing/one_fa_user defined')
     os.environ['common_name'] = one_fa_user
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertFalse(res, '1fa user attempting to 2fa must be denied')
Ejemplo n.º 14
0
 def test_1fa_user_attempts_2fa(self):
     """ A 1FA user trying to 2FA fails """
     # This is a weird test that stems from a 1FA user pretending to
     # have a Duo.
     try:
         one_fa_user = self.main_object.configfile.get('testing',
                                                       'one_fa_user')
     except (NoOptionError, NoSectionError):  # pragma: no cover
         return self.skipTest('No testing/one_fa_user defined')
     os.environ['common_name'] = one_fa_user
     os.environ['password'] = '******'
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertFalse(res, '1fa user attempting to 2fa must be denied')
Ejemplo n.º 15
0
 def test_1fa_user_good_pw(self):
     """ A 1FA user with a good password works """
     try:
         one_fa_user = self.main_object.configfile.get(
             'testing', 'one_fa_user')
     except (NoOptionError, NoSectionError):  # pragma: no cover
         return self.skipTest('No testing/one_fa_user defined')
     try:
         one_fa_pass = self.main_object.configfile.get(
             'testing', 'one_fa_pass')
     except (NoOptionError, NoSectionError):  # pragma: no cover
         return self.skipTest('No testing/one_fa_pass defined')
     os.environ['common_name'] = one_fa_user
     os.environ['password'] = one_fa_pass
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertTrue(res, '1fa user with good password gets accepted')
Ejemplo n.º 16
0
 def test_1fa_user_good_pw(self):
     """ A 1FA user with a good password works """
     if not self.main_object.configfile.has_section('testing'):
         return self.skipTest('No testing section defined')
     if not self.main_object.configfile.has_option('testing',
                                                   'one_fa_user'):
         return self.skipTest('No testing/one_fa_user defined')
     one_fa_user = self.main_object.configfile.get('testing', 'one_fa_user')
     if not self.main_object.configfile.has_option('testing',
                                                   'one_fa_pass'):
         return self.skipTest('No testing/one_fa_pass defined')
     one_fa_pass = self.main_object.configfile.get('testing', 'one_fa_pass')
     os.environ['common_name'] = one_fa_user
     os.environ['password'] = one_fa_pass
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertTrue(res, '1fa user with good password gets accepted')
Ejemplo n.º 17
0
 def test_1fa_user_good_pw(self):
     """ A 1FA user with a good password works """
     try:
         one_fa_user = self.main_object.configfile.get('testing',
                                                       'one_fa_user')
     except (NoOptionError, NoSectionError):  # pragma: no cover
         return self.skipTest('No testing/one_fa_user defined')
     try:
         one_fa_pass = self.main_object.configfile.get('testing',
                                                       'one_fa_pass')
     except (NoOptionError, NoSectionError):  # pragma: no cover
         return self.skipTest('No testing/one_fa_pass defined')
     os.environ['common_name'] = one_fa_user
     os.environ['password'] = one_fa_pass
     library = DuoOpenVPN()
     library.log_to_stdout = False
     res = library.main_authentication()
     self.assertTrue(res, '1fa user with good password gets accepted')
Ejemplo n.º 18
0
def main():
    """
        The main function.  Handles file-writing back to openvpn.
        All the good stuff is in class DuoOpenVPN
    """
    #
    # 'auth_control_file' is passed to this script via an
    # environmental variable.  It's an ephemeral file.
    # If we allow a user, put a 1 in the file.
    # If we deny a user, put a 0 in the file.
    #
    control_file_path = os.environ.get('auth_control_file')
    if control_file_path is None:
        # Not having this set is bad: we have no way to tell openvpn
        # what's happened.  Or, you're running the script by hand.
        # Either way, there's no point in continuing.  Get out.
        # We print to STDOUT because this is likely a human, instead
        # of an actual run.  It's possible that we should 'log' this.
        print('No auth_control_file env variable provided.')
        sys.exit(1)
    # There are many more environmental variables needed for this
    # whole process to work.  They are captured/realized farther down in
    # the stack, so, if this looks sad and short, it's intentional.
    # The env-variable work is in OpenVPNCredentials

    auth_object = DuoOpenVPN()
    if auth_object.main_authentication():
        writeout_value = str(1)
    else:
        writeout_value = str(0)

    try:
        with open(control_file_path, 'w') as filehandle:
            filehandle.write(writeout_value)
    except IOError:
        # I couldn't write to the file, so we can't tell openvpn what
        # happened.  There's nothing to do but error out.
        sys.exit(1)

    # we wrote out in the try, so we're done.
    sys.exit(0)
Ejemplo n.º 19
0
class TestDuoOpenVPNUnit(unittest.TestCase):
    """
        These are intended to exercise internal functions of the library's
        DuoOpenVPN class without going out to Duo.
    """

    testing_conffile = '/tmp/TestDuoOpenVPNUnit.txt'

    def setUp(self):
        """ Preparing test rig """
        # Our test cases depend on the setup of an object that reads
        # in the environment at the time we create an object of our
        # test class.  As such, we don't have a good setup here.
        # Each test will have to do a lot of situational setup.
        # That said, we make a garbage object just to get our library read:
        config = configparser.ConfigParser()
        config.add_section('duo-credentials')
        config.set('duo-credentials', 'IKEY', 'DI9QQ99X9MK4H99RJ9FF')
        config.set('duo-credentials', 'SKEY',
                   '2md9rw5xeyxt8c648dgkmdrg3zpvnhj5b596mgku')
        config.set('duo-credentials', 'HOST', 'api-9f134ff9.duosekurity.com')
        with open(self.testing_conffile, 'w') as configfile:
            config.write(configfile)
        with mock.patch.object(DuoOpenVPN,
                               'CONFIG_FILE_LOCATIONS',
                               new=[self.testing_conffile]):
            self.library = DuoOpenVPN()

    def tearDown(self):
        """ Clear the env so we don't impact other tests """
        try:
            os.unlink(self.testing_conffile)
        except OSError:  # pragma: no cover
            # ... else, there was nothing there (likely) ...
            if os.path.exists(self.testing_conffile):
                # ... but if there is, we couldn't delete it, so complain.
                raise

    def test_03_ingest_no_config_files(self):
        """ With no config files, get an exception """
        with mock.patch.object(DuoOpenVPN, 'CONFIG_FILE_LOCATIONS', new=[]):
            with self.assertRaises(IOError):
                self.library._ingest_config_from_file()

    def test_04_ingest_no_config_file(self):
        """ With all missing config files, get an exception """
        with mock.patch.object(DuoOpenVPN,
                               'CONFIG_FILE_LOCATIONS',
                               new=['/tmp/no-such-file.txt']):
            with self.assertRaises(IOError):
                self.library._ingest_config_from_file()

    def test_05_ingest_bad_config_file(self):
        """ With a bad config file, get an exception """
        with mock.patch.object(DuoOpenVPN,
                               'CONFIG_FILE_LOCATIONS',
                               new=['test/context.py']):
            with self.assertRaises(IOError):
                self.library._ingest_config_from_file()

    def test_06_ingest_config_from_file(self):
        """ With an actual config file, get a populated ConfigParser """
        test_reading_file = '/tmp/test-reader.txt'
        with open(test_reading_file, 'w') as filepointer:
            filepointer.write('[aa]\nbb = cc\n')
        filepointer.close()
        with mock.patch.object(
                DuoOpenVPN,
                'CONFIG_FILE_LOCATIONS',
                new=['/tmp/no-such-file.txt', test_reading_file]):
            result = self.library._ingest_config_from_file()
        os.remove(test_reading_file)
        self.assertIsInstance(result, configparser.ConfigParser,
                              'Did not create a config object')
        self.assertEqual(result.sections(), ['aa'],
                         'Should have found one configfile section.')
        self.assertEqual(result.options('aa'), ['bb'],
                         'Should have found one option.')
        self.assertEqual(result.get('aa', 'bb'), 'cc',
                         'Should have read a correct value.')

    def test_07_ingest_defaults(self):
        """ With a weak file, check our defaults """
        self.assertIn('ikey', self.library.duo_client_args)
        self.assertIn('skey', self.library.duo_client_args)
        self.assertIn('host', self.library.duo_client_args)
        self.assertFalse(self.library.failopen)
        self.assertFalse(self.library.event_send)
        self.assertEqual(self.library.event_facility, syslog.LOG_AUTH)
        self.assertEqual(self.library.duo_timeout, 300)

    def test_08_ingest_configs(self):
        """ With a strong file, check our imports """
        config = configparser.ConfigParser()
        config.add_section('duo-credentials')
        config.set('duo-credentials', 'IKEY', 'DI9QQ99X9MK4H99RJ9FF')
        config.set('duo-credentials', 'SKEY',
                   '2md9rw5xeyxt8c648dgkmdrg3zpvnhj5b596mgku')
        config.set('duo-credentials', 'HOST', 'api-9f134ff9.duosekurity.com')
        config.add_section('duo-behavior')
        config.set('duo-behavior', 'fail_open', 'True')
        config.set('duo-behavior', 'duo-timeout', '120')
        config.add_section('duo-openvpn')
        config.set('duo-openvpn', 'syslog-events-send', 'True')
        config.set('duo-openvpn', 'syslog-events-facility', 'local5')
        with open(self.testing_conffile, 'w') as configfile:
            config.write(configfile)
        with mock.patch.object(DuoOpenVPN,
                               'CONFIG_FILE_LOCATIONS',
                               new=[self.testing_conffile]):
            self.library = DuoOpenVPN()
        self.assertIn('ikey', self.library.duo_client_args)
        self.assertIn('skey', self.library.duo_client_args)
        self.assertIn('host', self.library.duo_client_args)
        self.assertTrue(self.library.failopen)
        self.assertEqual(self.library.duo_timeout, 120)
        self.assertTrue(self.library.event_send)
        self.assertEqual(self.library.event_facility, syslog.LOG_LOCAL5)

    def test_09_ingest_stupidity(self):
        """ With a terrible file, check our imports """
        config = configparser.ConfigParser()
        config.add_section('duo-credentials')
        config.set('duo-credentials', 'IKEY', 'DI9QQ99X9MK4H99RJ9FF')
        config.set('duo-credentials', 'SKEY',
                   '2md9rw5xeyxt8c648dgkmdrg3zpvnhj5b596mgku')
        config.set('duo-credentials', 'HOST', 'api-9f134ff9.duosekurity.com')
        config.add_section('duo-behavior')
        config.set('duo-behavior', 'duo-timeout', '-5')
        config.add_section('duo-openvpn')
        config.set('duo-openvpn', 'syslog-events-facility', 'junk')
        with open(self.testing_conffile, 'w') as configfile:
            config.write(configfile)
        with mock.patch.object(DuoOpenVPN,
                               'CONFIG_FILE_LOCATIONS',
                               new=[self.testing_conffile]):
            self.library = DuoOpenVPN()
        self.assertEqual(self.library.duo_timeout, 300)
        self.assertEqual(self.library.event_facility, syslog.LOG_AUTH)

    def test_10_log_nosend(self):
        ''' Test the log method failing to send '''
        self.library.event_send = False
        with mock.patch('syslog.openlog') as mock_openlog, \
                mock.patch('syslog.syslog') as mock_syslog:
            self.library.log('some message', {'foo': 5}, 'CRITICAL')
        mock_openlog.assert_not_called()
        mock_syslog.assert_not_called()

    def test_11_log_send(self):
        ''' Test the log method tries to send '''
        datetime_mock = mock.Mock(wraps=datetime.datetime)
        datetime_mock.utcnow.return_value = datetime.datetime(
            2020, 12, 25, 13, 14, 15, 123456)
        self.library.event_send = True
        self.library.event_facility = syslog.LOG_LOCAL1
        with mock.patch('syslog.openlog') as mock_openlog, \
                mock.patch('syslog.syslog') as mock_syslog, \
                mock.patch('datetime.datetime', new=datetime_mock), \
                mock.patch('os.getpid', return_value=12345), \
                mock.patch('socket.getfqdn', return_value='my.host.name'):
            self.library.log('some message', {'foo': 5}, 'CRITICAL')
        mock_openlog.assert_called_once_with(facility=syslog.LOG_LOCAL1)
        mock_syslog.assert_called_once()
        arg_passed_in = mock_syslog.call_args_list[0][0][0]
        json_sent = json.loads(arg_passed_in)
        details = json_sent['details']
        self.assertEqual(json_sent['category'], 'authentication')
        self.assertEqual(json_sent['processid'], 12345)
        self.assertEqual(json_sent['severity'], 'CRITICAL')
        self.assertIn('processname', json_sent)
        self.assertEqual(json_sent['timestamp'],
                         '2020-12-25T13:14:15.123456+00:00')
        self.assertEqual(json_sent['hostname'], 'my.host.name')
        self.assertEqual(json_sent['summary'], 'some message')
        self.assertEqual(json_sent['source'], 'openvpn')
        self.assertEqual(json_sent['tags'], ['vpn', 'duosecurity'])
        self.assertEqual(details, {'foo': 5})

    def test_20_auth_bogus_user(self):
        """ A bogus user is denied """
        with mock.patch.object(DuoOpenVPN, 'log') as mock_log:
            with mock.patch.object(OpenVPNCredentials, 'load_variables_from_environment',
                                   side_effect=ValueError), \
                    mock.patch('sys.stderr', new=StringIO()) as fake_out:
                res = self.library.main_authentication()
        self.assertFalse(res, 'invalid environment must be denied access')
        # Check the call_args - [1] is the kwargs.
        self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
        self.assertEqual(mock_log.call_args[1]['details']['success'], 'false')
        self.assertIn('Traceback', fake_out.getvalue())

    def test_21_auth_bad_iam(self):
        """ A user is denied when IAM is unreachable """
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        with mock.patch.object(DuoOpenVPN, 'log') as mock_log:
            with mock.patch('iamvpnlibrary.IAMVPNLibrary',
                            side_effect=RuntimeError):
                res = self.library.main_authentication()
        self.assertFalse(res, 'Disconnected IAM must be denied access')
        # Check the call_args - [1] is the kwargs.
        self.assertEqual(mock_log.call_args[1]['details']['success'], 'false')

    def test_22_auth_disabled_user(self):
        """ A disabled user is denied """
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        with mock.patch.object(DuoOpenVPN, 'log') as mock_log:
            with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam:
                iam_instance = mock_iam.return_value
                with mock.patch.object(iam_instance,
                                       'user_allowed_to_vpn',
                                       return_value=False):
                    res = self.library.main_authentication()
        self.assertFalse(res, 'Disallowed user must be denied access')
        # Check the call_args - [1] is the kwargs.
        self.assertEqual(mock_log.call_args[1]['details']['success'], 'false')

    def test_23_auth_1fa_garbage_pw(self):
        """ 1fa user with a 2fa / bonkers 'password' """
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        os.environ['password'] = '******'
        with mock.patch.object(DuoOpenVPN, 'log') as mock_log:
            with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \
                    mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn',
                                      return_value=True):
                with mock.patch.object(mock_iam.return_value,
                                       'does_user_require_vpn_mfa',
                                       return_value=False):
                    res = self.library.main_authentication()
        self.assertFalse(
            res,
            '1fa user with stupid colliding passwords must be denied access')
        # Check the call_args - [1] is the kwargs.
        self.assertEqual(mock_log.call_args[1]['details']['success'], 'false')

    def test_24_auth_1fa_bad_pw(self):
        """ 1fa user with a bad password """
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        os.environ['password'] = '******'
        with mock.patch.object(DuoOpenVPN, 'log') as mock_log:
            with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \
                    mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn',
                                      return_value=True):
                with mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa',
                                       return_value=False), \
                        mock.patch.object(mock_iam.return_value, 'non_mfa_vpn_authentication',
                                          return_value=False):
                    res = self.library.main_authentication()
        self.assertFalse(
            res, '1fa user with a wrong password must be denied access')
        # Check the call_args - [1] is the kwargs.
        self.assertEqual(mock_log.call_args[1]['details']['success'], 'false')

    def test_25_auth_1fa_good_pw(self):
        """ 1fa user with a good password """
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        os.environ['password'] = '******'
        with mock.patch.object(DuoOpenVPN, 'log') as mock_log:
            with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \
                    mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn',
                                      return_value=True):
                with mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa',
                                       return_value=False), \
                        mock.patch.object(mock_iam.return_value, 'non_mfa_vpn_authentication',
                                          return_value=True):
                    res = self.library.main_authentication()
        self.assertTrue(res, '1fa user with the right password can get in')
        # Check the call_args - [1] is the kwargs.
        self.assertEqual(mock_log.call_args[1]['details']['success'], 'true')

    def test_26_auth_2fa_no_load(self):
        """ 2fa user who we can't load into Duo """
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        os.environ['password'] = '******'
        with mock.patch.object(DuoOpenVPN, 'log') as mock_log:
            with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \
                    mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn',
                                      return_value=True), \
                    mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa',
                                      return_value=True):
                with mock.patch('duo_auth.DuoAPIAuth'), \
                        mock.patch.object(DuoAPIAuth, 'load_user_to_verify', return_value=False):
                    res = self.library.main_authentication()
        self.assertFalse(
            res, '2fa user is denied when we cannot load up our Duo search')
        # Check the call_args - [1] is the kwargs.
        self.assertEqual(mock_log.call_args[1]['details']['success'], 'false')

    def test_27_auth_2fa_fail_to_auth(self):
        """ 2fa user who we can't load into Duo """
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        os.environ['password'] = '******'
        with mock.patch.object(DuoOpenVPN, 'log') as mock_log:
            with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \
                    mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn',
                                      return_value=True), \
                    mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa',
                                      return_value=True):
                with mock.patch('duo_auth.DuoAPIAuth'), \
                        mock.patch.object(DuoAPIAuth, 'load_user_to_verify', return_value=True), \
                        mock.patch.object(DuoAPIAuth, 'main_auth', side_effect=IOError), \
                        mock.patch('sys.stderr', new=StringIO()) as fake_out:
                    res = self.library.main_authentication()
        self.assertFalse(res, '2fa user is denied when Duo errors out on us')
        # Check the call_args - [1] is the kwargs.
        self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
        self.assertEqual(mock_log.call_args[1]['details']['success'], 'false')
        self.assertIn('Traceback', fake_out.getvalue())

    def test_27_auth_2fa_duo_deny(self):
        """ 2fa user who is denied by Duo """
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        os.environ['password'] = '******'
        with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \
                mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn',
                                  return_value=True), \
                mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa',
                                  return_value=True):
            with mock.patch('duo_auth.DuoAPIAuth'), \
                    mock.patch.object(DuoAPIAuth, 'load_user_to_verify', return_value=True), \
                    mock.patch.object(DuoAPIAuth, 'main_auth', return_value=False):
                res = self.library.main_authentication()
        self.assertFalse(res, '2fa user is denied when Duo denies them')

    def test_27_auth_2fa_duo_allow(self):
        """ 2fa user who is allowed by Duo """
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        os.environ['password'] = '******'
        with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \
                mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn',
                                  return_value=True), \
                mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa',
                                  return_value=True):
            with mock.patch('duo_auth.DuoAPIAuth'), \
                    mock.patch.object(DuoAPIAuth, 'load_user_to_verify', return_value=True), \
                    mock.patch.object(DuoAPIAuth, 'main_auth', return_value=True):
                res = self.library.main_authentication()
        self.assertTrue(res, '2fa user is allowed when Duo allows them')