Add exponential backoff of login attempts.
This commit is contained in:
parent
066b3ed8f0
commit
2dcdd7ba5b
5 changed files with 46 additions and 1 deletions
|
@ -76,6 +76,8 @@ class User(BaseModel):
|
||||||
organization = BooleanField(default=False, index=True)
|
organization = BooleanField(default=False, index=True)
|
||||||
robot = BooleanField(default=False, index=True)
|
robot = BooleanField(default=False, index=True)
|
||||||
invoice_email = BooleanField(default=False)
|
invoice_email = BooleanField(default=False)
|
||||||
|
invalid_login_attempts = IntegerField(default=0)
|
||||||
|
last_invalid_login = DateTimeField(default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
class TeamRole(BaseModel):
|
class TeamRole(BaseModel):
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import bcrypt
|
import bcrypt
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from data.database import *
|
from data.database import *
|
||||||
from util.validation import *
|
from util.validation import *
|
||||||
from util.names import format_robot_username
|
from util.names import format_robot_username
|
||||||
|
from util.backoff import exponential_backoff
|
||||||
|
|
||||||
|
|
||||||
|
EXPONENTIAL_BACKOFF_SCALE = timedelta(seconds=1)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -68,6 +73,12 @@ class TooManyUsersException(DataModelException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TooManyLoginAttemptsException(Exception):
|
||||||
|
def __init__(self, message, retry_after):
|
||||||
|
super(TooManyLoginAttemptsException, self).__init__(message)
|
||||||
|
self.retry_after = retry_after
|
||||||
|
|
||||||
|
|
||||||
def is_create_user_allowed():
|
def is_create_user_allowed():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -551,11 +562,30 @@ def verify_user(username_or_email, password):
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
if fetched.invalid_login_attempts > 0:
|
||||||
|
can_retry_at = exponential_backoff(fetched.invalid_login_attempts, EXPONENTIAL_BACKOFF_SCALE,
|
||||||
|
fetched.last_invalid_login)
|
||||||
|
|
||||||
|
if can_retry_at > now:
|
||||||
|
retry_after = can_retry_at - now
|
||||||
|
raise TooManyLoginAttemptsException('Too many login attempts.', retry_after.total_seconds())
|
||||||
|
|
||||||
if (fetched.password_hash and
|
if (fetched.password_hash and
|
||||||
bcrypt.hashpw(password, fetched.password_hash) ==
|
bcrypt.hashpw(password, fetched.password_hash) ==
|
||||||
fetched.password_hash):
|
fetched.password_hash):
|
||||||
|
|
||||||
|
if fetched.invalid_login_attempts > 0:
|
||||||
|
fetched.invalid_login_attempts = 0
|
||||||
|
fetched.save()
|
||||||
|
|
||||||
return fetched
|
return fetched
|
||||||
|
|
||||||
|
fetched.invalid_login_attempts += 1
|
||||||
|
fetched.last_invalid_login = now
|
||||||
|
fetched.save()
|
||||||
|
|
||||||
# We weren't able to authorize the user
|
# We weren't able to authorize the user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,14 @@ def handle_api_error(error):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.app_errorhandler(model.TooManyLoginAttemptsException)
|
||||||
|
@crossdomain(origin='*', headers=['Authorization', 'Content-Type'])
|
||||||
|
def handle_too_many_login_attempts(error):
|
||||||
|
response = make_response('Too many login attempts', 429)
|
||||||
|
response.headers['Retry-After'] = int(error.retry_after)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def resource(*urls, **kwargs):
|
def resource(*urls, **kwargs):
|
||||||
def wrapper(api_resource):
|
def wrapper(api_resource):
|
||||||
if not api_resource:
|
if not api_resource:
|
||||||
|
|
Binary file not shown.
5
util/backoff.py
Normal file
5
util/backoff.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
def exponential_backoff(attempts, scaling_factor, base):
|
||||||
|
backoff = 5 * (pow(2, attempts) - 1)
|
||||||
|
backoff_time = backoff * scaling_factor
|
||||||
|
retry_at = backoff_time/10 + base
|
||||||
|
return retry_at
|
Reference in a new issue