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')
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
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')
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'))
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
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)
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
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)