Example #1
0
    def testHOTPRateLimit(self):
        logger.debug('Running testHOTPRateLimit')

        backends = getBackends()
        # Save custom state for HOTP user, as some backends rely on it to trigger HOTP mode
        state = totpcgi.GAUserState()
        state.counter = 1
        setCustomState(state, 'hotp')

        gau = totpcgi.GAUser('hotp', backends)
        secret = backends.secret_backend.get_user_secret(gau.user)

        hotp = pyotp.HOTP(secret.otp.secret)
        token = hotp.at(1)
        self.assertEqual(gau.verify_token(token), 'Valid HOTP token used')
        # counter is now at 2

        token = '555555'

        # We now fail 4 times consecutively
        with self.assertRaisesRegex(totpcgi.VerifyFailed,
                                    'HOTP token failed to verify'):
            gau.verify_token(token)
        with self.assertRaisesRegex(totpcgi.VerifyFailed,
                                    'HOTP token failed to verify'):
            gau.verify_token(token)
        with self.assertRaisesRegex(totpcgi.VerifyFailed,
                                    'HOTP token failed to verify'):
            gau.verify_token(token)
        with self.assertRaisesRegex(totpcgi.VerifyFailed,
                                    'HOTP token failed to verify'):
            gau.verify_token(token)

        # We should now get a rate-limited error
        with self.assertRaisesRegex(totpcgi.VerifyFailed, 'Rate-limit'):
            gau.verify_token(token)

        # Same with a valid token
        with self.assertRaisesRegex(totpcgi.VerifyFailed, 'Rate-limit'):
            gau.verify_token(hotp.at(2))

        # Make sure we recover from rate-limiting correctly
        old_timestamp = secret.timestamp - (31 + (secret.rate_limit[1] * 10))
        state = totpcgi.GAUserState()
        state.fail_timestamps = [
            old_timestamp, old_timestamp, old_timestamp, old_timestamp
        ]
        state.counter = 2
        setCustomState(state, 'hotp')

        with self.assertRaisesRegex(totpcgi.VerifyFailed,
                                    'HOTP token failed to verify'):
            gau.verify_token(token)

        # Valid token should work, too
        setCustomState(state, 'hotp')
        self.assertEqual(gau.verify_token(hotp.at(2)), 'Valid HOTP token used')
        cleanState('hotp')
Example #2
0
    def get_user_state(self, user):

        userid = get_user_id(self.conn, user)

        state = totpcgi.GAUserState()

        logger.debug('Creating advisory lock for userid=%s' % userid)

        cur = self.conn.cursor()
        cur.execute('SELECT pg_advisory_lock(%s)', (userid, ))
        self.locks[user] = userid

        cur.execute(
            '''
            SELECT timestamp, success
              FROM timestamps
             WHERE userid = %s''', (userid, ))

        for (timestamp, success) in cur.fetchall():
            if success:
                state.success_timestamps.append(timestamp)
            else:
                state.fail_timestamps.append(timestamp)

        cur.execute(
            '''
            SELECT token
              FROM used_scratch_tokens
             WHERE userid = %s''', (userid, ))

        for (token, ) in cur.fetchall():
            state.used_scratch_tokens.append(token)

        return state
Example #3
0
    def testValidToken(self):
        logger.debug('Running testValidToken')

        gau = getValidUser()
        backends = getBackends()
        secret = backends.secret_backend.get_user_secret(gau.user)

        totp = pyotp.TOTP(secret.otp.secret)
        token = totp.now()
        self.assertEqual(gau.verify_token(token), 'Valid TOTP token used')

        # try using it again
        with self.assertRaisesRegexp(totpcgi.VerifyFailed, 'been used once'):
            gau.verify_token(token)

        # and again, to make sure it is preserved in state
        with self.assertRaisesRegexp(totpcgi.VerifyFailed, 'been used once'):
            gau.verify_token(token)

        gau = totpcgi.GAUser('hotp', backends)
        # Save custom state for HOTP user, as some backends rely on it to trigger HOTP mode
        state = totpcgi.GAUserState()
        state.counter = 0
        setCustomState(state, 'hotp')

        hotp = pyotp.HOTP(secret.otp.secret)
        token = hotp.at(0)
        self.assertEqual(gau.verify_token(token), 'Valid HOTP token used')

        # make sure the counter now validates at 1 and 2
        self.assertEqual(gau.verify_token(hotp.at(1)), 'Valid HOTP token used')
        self.assertEqual(gau.verify_token(hotp.at(2)), 'Valid HOTP token used')

        # make sure trying "1" or "2" fails now
        with self.assertRaisesRegexp(totpcgi.VerifyFailed, 'HOTP token failed to verify'):
            gau.verify_token(hotp.at(1))
        with self.assertRaisesRegexp(totpcgi.VerifyFailed, 'HOTP token failed to verify'):
            gau.verify_token(hotp.at(2))

        # but we're good to go at 3
        self.assertEqual(gau.verify_token(hotp.at(3)), 'Valid HOTP token used')

        # and we're good to go with 7, which is max window size
        self.assertEqual(gau.verify_token(hotp.at(7)), 'Valid HOTP token within window size used')

        # Trying with "5" should fail now
        with self.assertRaisesRegexp(totpcgi.VerifyFailed, 'HOTP token failed to verify'):
            gau.verify_token(hotp.at(5))

        # but we're good to go at 8
        self.assertEqual(gau.verify_token(hotp.at(8)), 'Valid HOTP token used')

        # should fail with 13, which is beyond window size of 9+3
        with self.assertRaisesRegexp(totpcgi.VerifyFailed, 'HOTP token failed to verify'):
            gau.verify_token(hotp.at(13))

        cleanState('hotp')
Example #4
0
    def testTOTPRateLimit(self):
        logger.debug('Running testTOTPRateLimit')

        gau = getValidUser()

        backends = getBackends()
        secret = backends.secret_backend.get_user_secret(gau.user)
        token = '555555'

        # We now fail 4 times consecutively
        with self.assertRaisesRegex(totpcgi.VerifyFailed,
                                    'TOTP token failed to verify'):
            gau.verify_token(token)
        with self.assertRaisesRegex(totpcgi.VerifyFailed,
                                    'TOTP token failed to verify'):
            gau.verify_token(token)
        with self.assertRaisesRegex(totpcgi.VerifyFailed,
                                    'TOTP token failed to verify'):
            gau.verify_token(token)
        with self.assertRaisesRegex(totpcgi.VerifyFailed,
                                    'TOTP token failed to verify'):
            gau.verify_token(token)

        # We should now get a rate-limited error
        with self.assertRaisesRegex(totpcgi.VerifyFailed, 'Rate-limit'):
            gau.verify_token(token)

        # Same with a valid token
        with self.assertRaisesRegex(totpcgi.VerifyFailed, 'Rate-limit'):
            gau.verify_token(secret.get_totp_token())

        # Make sure we recover from rate-limiting correctly
        old_timestamp = secret.timestamp - (31 + (secret.rate_limit[1] * 10))
        state = totpcgi.GAUserState()
        state.fail_timestamps = [
            old_timestamp, old_timestamp, old_timestamp, old_timestamp
        ]
        setCustomState(state)

        with self.assertRaisesRegex(totpcgi.VerifyFailed,
                                    'TOTP token failed to verify'):
            gau.verify_token(token)

        # Valid token should work, too
        setCustomState(state)
        ret = gau.verify_token(secret.get_totp_token())
        self.assertIn(ret, ('Valid TOTP token used',
                            'Valid TOTP token within window size used'))
Example #5
0
    def get_user_state(self, user):

        userid = get_user_id(self.conn, user)

        state = totpcgi.GAUserState()

        logger.debug('Acquiring lock for userid=%s', userid)

        cur = self.conn.cursor()
        cur.execute('SELECT GET_LOCK(%s,180)', (userid, ))
        self.locks[user] = userid

        cur.execute(
            '''
            SELECT timestamp, success
              FROM timestamps
             WHERE userid = %s''', (userid, ))

        for (timestamp, success) in cur.fetchall():
            if success:
                state.success_timestamps.append(timestamp)
            else:
                state.fail_timestamps.append(timestamp)

        cur.execute(
            '''
            SELECT token
              FROM used_scratch_tokens
             WHERE userid = %s''', (userid, ))

        for (itoken, ) in cur.fetchall():
            token = str(itoken).zfill(8)
            logger.debug('Found a used scratch token: %s', token)
            state.used_scratch_tokens.append(token)

        # Now try to load counter info, if we have that table
        if self.has_counters:
            cur.execute(
                '''
                SELECT counter
                  FROM counters
                 WHERE userid = %s''', (userid, ))

            row = cur.fetchone()
            if row and row[0] >= 0:
                state.counter = row[0]

        return state
Example #6
0
    def get_user_state(self, user):
        state = totpcgi.GAUserState()

        import json

        # load the state file and keep it locked while we do verification
        state_file = os.path.join(self.state_dir, user) + '.json'
        logger.debug('Loading user state from: %s' % state_file)
        
        # For totpcgiprov and totpcgi to be able to write to the same state
        # file, we have to create it world-writable. Since we have restricted
        # permissions on the parent directory (totpcgi:totpcgiprov), plus
        # selinux labels in place, this should keep this safe from tampering.
        os.umask(0000)

        # we exclusive-lock the file to prevent race conditions resulting
        # in potential token reuse.
        if os.access(state_file, os.W_OK):
            logger.debug('%s exists, opening r+' % state_file)
            fh = open(state_file, 'r+')
            logger.debug('Locking state file for user %s' % user)
            lockf(fh, LOCK_EX)
            try:
                js = json.load(fh)

                logger.debug('loaded state=%s' % js)

                state.fail_timestamps = js['fail_timestamps']
                state.success_timestamps = js['success_timestamps']
                state.used_scratch_tokens = js['used_scratch_tokens']

                if 'counter' in js:
                    state.counter = js['counter']

            except Exception, ex:
                # We fail out of caution, though if someone wanted to 
                # screw things up, they could have done so without making
                # the file un-parseable by json -- all they need to do is to
                # erase the file.
                logger.debug('Parsing json failed with: %s' % ex)
                logger.debug('Unlocking state file for user %s' % user)
                lockf(fh, LOCK_UN)
                raise totpcgi.UserStateError(
                    'Error parsing the state file for: %s' % user)

            fh.seek(0)
Example #7
0
    def get_user_state(self, user):

        userid = get_user_id(self.conn, user)

        state = totpcgi.GAUserState()

        logger.debug('Creating advisory lock for userid=%s' % userid)

        cur = self.conn.cursor()
        cur.execute('SELECT pg_advisory_lock(%s)', (userid, ))
        self.locks[user] = userid

        cur.execute(
            '''
            SELECT timestamp, success
              FROM timestamps
             WHERE userid = %s''', (userid, ))

        for (timestamp, success) in cur.fetchall():
            if success:
                state.success_timestamps.append(timestamp)
            else:
                state.fail_timestamps.append(timestamp)

        cur.execute(
            '''
            SELECT token
              FROM used_scratch_tokens
             WHERE userid = %s''', (userid, ))

        for (token, ) in cur.fetchall():
            state.used_scratch_tokens.append(token)

        # Now try to load counter info, if we have that table
        if self.has_counters:
            cur.execute(
                '''
                SELECT counter
                  FROM counters
                 WHERE userid = %s''', (userid, ))

            row = cur.fetchone()
            if row and row[0] >= 0:
                state.counter = row[0]

        return state
Example #8
0
    def get_user_state(self, user):
        state = totpcgi.GAUserState()

        import json

        # load the state file and keep it locked while we do verification
        state_file = os.path.join(self.state_dir, user) + '.json'
        logger.debug('Loading user state from: %s' % state_file)
        
        # Don't let anyone but ourselves see the contents of the state file
        os.umask(0077)

        # we exclusive-lock the file to prevent race conditions resulting
        # in potential token reuse.
        if os.access(state_file, os.R_OK):
            logger.debug('%s exists, opening r+' % state_file)
            fh = open(state_file, 'r+')
            logger.debug('Locking state file for user %s' % user)
            flock(fh, LOCK_EX)
            try:
                js = json.load(fh)

                logger.debug('loaded state=%s' % js)

                state.fail_timestamps     = js['fail_timestamps']
                state.success_timestamps  = js['success_timestamps']
                state.used_scratch_tokens = js['used_scratch_tokens']

            except Exception, ex:
                # We fail out of caution, though if someone wanted to 
                # screw things up, they could have done so without making
                # the file un-parseable by json -- all they need to do is to
                # erase the file.
                logger.debug('Parsing json failed with: %s' % ex)
                logger.debug('Unlocking state file for user %s' % user)
                flock(fh, LOCK_UN)
                raise totpcgi.UserStateError(
                        'Error parsing the state file for: %s' % user)

            fh.seek(0)