From 2dcdd7ba5b93d6deece46760c5b219f21be264fe Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Tue, 2 Sep 2014 15:27:05 -0400 Subject: [PATCH] Add exponential backoff of login attempts. --- data/database.py | 2 ++ data/model/legacy.py | 32 +++++++++++++++++++++++++++++++- endpoints/api/__init__.py | 8 ++++++++ test/data/test.db | Bin 614400 -> 231424 bytes util/backoff.py | 5 +++++ 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 util/backoff.py diff --git a/data/database.py b/data/database.py index 349ad1b58..69932273d 100644 --- a/data/database.py +++ b/data/database.py @@ -76,6 +76,8 @@ class User(BaseModel): organization = BooleanField(default=False, index=True) robot = BooleanField(default=False, index=True) invoice_email = BooleanField(default=False) + invalid_login_attempts = IntegerField(default=0) + last_invalid_login = DateTimeField(default=datetime.utcnow) class TeamRole(BaseModel): diff --git a/data/model/legacy.py b/data/model/legacy.py index 52723bd11..2e703b003 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -1,12 +1,17 @@ import bcrypt import logging -import datetime import dateutil.parser import json +from datetime import datetime, timedelta + from data.database import * from util.validation import * from util.names import format_robot_username +from util.backoff import exponential_backoff + + +EXPONENTIAL_BACKOFF_SCALE = timedelta(seconds=1) logger = logging.getLogger(__name__) @@ -68,6 +73,12 @@ class TooManyUsersException(DataModelException): 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(): return True @@ -551,11 +562,30 @@ def verify_user(username_or_email, password): except User.DoesNotExist: 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 bcrypt.hashpw(password, fetched.password_hash) == fetched.password_hash): + + if fetched.invalid_login_attempts > 0: + fetched.invalid_login_attempts = 0 + fetched.save() + return fetched + fetched.invalid_login_attempts += 1 + fetched.last_invalid_login = now + fetched.save() + # We weren't able to authorize the user return None diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index e8dab28dc..854c3cad1 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -87,6 +87,14 @@ def handle_api_error(error): 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 wrapper(api_resource): if not api_resource: diff --git a/test/data/test.db b/test/data/test.db index 34882e11701b07d1a0e1ea1631cdc6636489f033..3e631b5d72e5c2c290e95affc3a9794853a6a573 100644 GIT binary patch delta 19857 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 delta 21587 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 diff --git a/util/backoff.py b/util/backoff.py new file mode 100644 index 000000000..15429936e --- /dev/null +++ b/util/backoff.py @@ -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