
Ramon, On 02/03/2017 05:39 PM, ramonn@linux.vnet.ibm.com wrote:
From: Ramon Medeiros <ramonn@linux.vnet.ibm.com>
To prevent brute force attack, creates a mechanism to allow 3 tries first. After that, a timeout will start and will be added 30 seconds for each failed try in a row.
Signed-off-by: Ramon Medeiros <ramonn@linux.vnet.ibm.com> --- Changes:
v5: Friendly error message Set maximum tries to 3
v4: Use API.json for input validation
v3: Improve error handling on login page
v2: Set timeout by user, ip and session id. This will avoid trouble with users using the same ip, like NAT.
src/wok/API.json | 25 +++++++++++++++++- src/wok/i18n.py | 7 +++-- src/wok/root.py | 69 +++++++++++++++++++++++++++++++++++++++++------- ui/js/src/wok.login.js | 19 ++++++++----- ui/pages/i18n.json.tmpl | 5 +++- ui/pages/login.html.tmpl | 6 ++--- 6 files changed, 107 insertions(+), 24 deletions(-)
diff --git a/src/wok/API.json b/src/wok/API.json index 8965db9..3f7bfd7 100644 --- a/src/wok/API.json +++ b/src/wok/API.json @@ -2,5 +2,28 @@ "$schema": "http://json-schema.org/draft-03/schema#", "title": "Wok API", "description": "Json schema for Wok API", - "type": "object" + "type": "object", + "properties": { + "wokroot_login": { + "type": "object", + "properties": { + "username": { + "description": "Username", + "required": true, + "type": "string", + "minLength": 1, + "error": "WOKAUTH0003E" + }, + "password": { + "description": "Password", + "required": true, + "type": "string", + "minLength": 1, + "error": "WOKAUTH0006E" + } + }, + "additionalProperties": false, + "error": "WOKAUTH0007E" + } + } } diff --git a/src/wok/i18n.py b/src/wok/i18n.py index 935c9c1..3cf669e 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -38,10 +38,13 @@ messages = { "WOKASYNC0003E": _("Timeout of %(seconds)s seconds expired while running task '%(task)s."), "WOKASYNC0004E": _("Unable to kill task due error: %(err)s"),
- "WOKAUTH0001E": _("Authentication failed for user '%(username)s'. [Error code: %(code)s]"), + "WOKAUTH0001E": _("The username or password you entered is incorrect. Please try again"),
Create a new error code to handle the above message and keep the former one as is as it is used in auth.py Then:
"WOKAUTH0002E": _("You are not authorized to access Wok. Please, login first."), - "WOKAUTH0003E": _("Specify %(item)s to login into Wok."), + "WOKAUTH0003E": _("Specify username to login into Wok."), + "WOKAUTH0004E": _("You have failed to login in too much attempts. Please, wait for %(seconds)s seconds to try again."), "WOKAUTH0005E": _("Invalid LDAP configuration: %(item)s : %(value)s"), + "WOKAUTH0006E": _("Specify password to login into Wok."), + "WOKAUTH0007E": _("You need to specify username and password to login into Wok."),
"WOKLOG0001E": _("Invalid filter parameter. Filter parameters allowed: %(filters)s"), "WOKLOG0002E": _("Creation of log file failed: %(err)s"), diff --git a/src/wok/root.py b/src/wok/root.py index 080b7f0..c75a44a 100644 --- a/src/wok/root.py +++ b/src/wok/root.py @@ -1,7 +1,7 @@ # # Project Wok # -# Copyright IBM Corp, 2015-2016 +# Copyright IBM Corp, 2015-2017 # # Code derived from Project Kimchi # @@ -21,7 +21,9 @@ def login(self, *args): + def _raise_timeout(user_id): + length = self.failed_logins[user_ip_sid]["count"] + timeout = (length - 2) * 30 + details = e = UnauthorizedError("WOKAUTH0004E", + {"seconds": timeout}) + log_request(code, params, details, method, 403) + raise cherrypy.HTTPError(403, e.message) details = None method = 'POST' code = self.getRequestMessage(method, 'login')
try: params = parse_request() + validate_params(params, self, "login") username = params['username'] password = params['password'] - except KeyError, item: - details = e = MissingParameter('WOKAUTH0003E', {'item': str(item)}) - log_request(code, params, details, method, 400) - raise cherrypy.HTTPError(400, e.message) + except WokException, e: + details = e + status = e.getHttpStatusCode() + raise cherrypy.HTTPError(status, e.message) + + # get authentication info + remote_ip = cherrypy.request.remote.ip + session_id = str(cherrypy.session.originalid) + user_ip_sid = re.escape(username + remote_ip + session_id) + + # check for repetly + count = self.failed_logins.get(user_ip_sid, {"count": 0}).get("count") + if count >= 3: + + # verify if timeout is still valid + last_try = self.failed_logins[user_ip_sid]["time"] + if time.time() < (last_try + ((count - 2) * 30)): + _raise_timeout(user_ip_sid) + else: + self.failed_logins.pop(user_ip_sid)
try: status = 200 user_info = auth.login(username, password) + + # user logged sucessfuly: reset counters + if self.failed_logins.get(user_ip_sid) != None: + self.failed_logins.pop(user_ip_sid) except cherrypy.HTTPError, e: - status = e.status + + # store time and prevent too much tries + if self.failed_logins.get(user_ip_sid) == None: + self.failed_logins[user_ip_sid] = {"time": time.time(), + "ip": remote_ip, + "session_id": session_id, + "username": username, + "count": 1} + else: + # tries take more than 30 seconds between each one: do not + # increase count + if (time.time() - + self.failed_logins[user_ip_sid]["time"]) < 30: + + self.failed_logins[user_ip_sid]["time"] = time.time() + self.failed_logins[user_ip_sid]["count"] += 1 + + # more than 3 fails: raise error + if self.failed_logins[user_ip_sid]["count"] >= 3: + _raise_timeout(user_ip_sid)
raise
raise AuthorizationError(<new error code>)
finally: log_request(code, params, details, method, status) diff --git a/ui/js/src/wok.login.js b/ui/js/src/wok.login.js index 666a339..9e2a392 100644