diff --git a/authserv/ratelimit.py b/authserv/ratelimit.py index e1561baa127bc17e442867aaba068c530974c14b..ff4f6a085d25f367cd006574b1f68a92c759ebd7 100644 --- a/authserv/ratelimit.py +++ b/authserv/ratelimit.py @@ -144,7 +144,8 @@ class BlackList(object): mc.set(key, 'true', time=self.ttl) -def blacklist_on_auth_failure(key_fn, count=0, period=0, ttl=0, check_wl=False): +def blacklist_on_auth_failure(key_fn, count=0, period=0, ttl=0, check_wl=False, + bl_return_value=protocol.ERR_AUTHENTICATION_FAILURE): """Blacklist authentication failures. The wrapped function should return one of the error codes from @@ -168,7 +169,7 @@ def blacklist_on_auth_failure(key_fn, count=0, period=0, ttl=0, check_wl=False): if ((not check_wl or not whitelisted(key)) and not bl.check(app.memcache, key)): app.logger.debug('blacklisted %s', key) - return protocol.ERR_AUTHENTICATION_FAILURE + return bl_return_value result = fn(*args, **kwargs) if result != protocol.OK: bl.incr(app.memcache, key) diff --git a/authserv/server.py b/authserv/server.py index 1e45bb9c018e3441e7c8785794998bcf6aec7f22..5124d8851901d7614b3e9bf98b4317548e066dcc 100644 --- a/authserv/server.py +++ b/authserv/server.py @@ -9,15 +9,18 @@ from authserv import protocol from authserv.ratelimit import * from flask import Flask, request, abort, make_response +_blacklisted = (protocol.ERR_AUTHENTICATION_FAILURE, None) -@blacklist_on_auth_failure(key_from_args(0), count=5, period=600, ttl=43200) +@blacklist_on_auth_failure(key_from_args(0), count=5, period=600, ttl=43200, + bl_return_value=_blacklisted) @blacklist_on_auth_failure(key_from_args(5), count=5, period=600, ttl=43200, - check_wl=True) + check_wl=True, bl_return_value=_blacklisted) def _auth(username, service, shard, password, otp_token, source_ip): user = app.userdb.get_user(username, service, shard) if not user: - return protocol.ERR_AUTHENTICATION_FAILURE - return auth.authenticate(user, service, password, otp_token) + return _blacklisted + return (auth.authenticate(user, service, password, otp_token), + user.get_shard()) # Quick clarification on the rate limits: 'username' is the one that's @@ -47,7 +50,7 @@ def do_auth(): abort(400) try: - result = _auth(username, service, shard, password, otp_token, source_ip) + result, _ = _auth(username, service, shard, password, otp_token, source_ip) except Exception, e: app.logger.exception('Unexpected exception in authenticate()') abort(500) @@ -63,6 +66,40 @@ def do_auth(): return response +_default_port_map = {'imap': 143, 'pop3': 110} + +@app.route('/auth', methods=('GET',)) +def do_nginx_http_auth(): + service = app.config.get('NGINX_AUTH_SERVICE', 'mail') + username = request.environ.get('HTTP_AUTH_USER') + password = request.environ.get('HTTP_AUTH_PASS') + source_ip = request.environ.get('HTTP_CLIENT_IP') + protocol = request.environ.get('HTTP_AUTH_PROTOCOL') + try: + n_attempt = int(request.environ.get('HTTP_AUTH_LOGIN_ATTEMPT')) + except ValueError: + n_attempt = 1 + + try: + auth_status, shard = _auth( + username, service, None, password, None, source_ip) + except Exception, e: + app.logger.exception('Unexpected exception in authenticate()') + abort(500) + + response = make_response('') + if auth_status == 'OK': + response.headers['Auth-Status'] = 'OK' + response.headers['Auth-Server'] = shard + response.headers['Auth-Port'] = str( + app.config.get('NGINX_AUTH_PORT_MAP', _default_port_map)[protocol]) + else: + response.headers['Auth-Status'] = 'Invalid login or password' + if n_attempt <= 3: + response.headers['Auth-Wait'] = '3' + return response + + def create_app(userdb=None, mc=None): app.config.from_envvar('APP_CONFIG', silent=True) diff --git a/authserv/test/test_server.py b/authserv/test/test_server.py index 28603244032e79ea77063c554913f662c8cba811..2008383e6d3cb441cdb3b1cc566f74f2ac5fb560 100644 --- a/authserv/test/test_server.py +++ b/authserv/test/test_server.py @@ -183,3 +183,27 @@ class ServerTest(unittest.TestCase): self.assertEquals(200, response.status_code) self.assertEquals(protocol.OK, response.data) + def test_nginx_http_auth_ok(self): + response = self.app.get( + '/auth', headers={ + 'Auth-User': 'user', + 'Auth-Pass': 'pass', + 'Client-IP': '127.0.0.1', + 'Auth-Protocol': 'imap', + 'Auth-Login-Attempt': '1', + }) + self.assertEquals(200, response.status_code) + self.assertEquals('OK', response.headers['Auth-Status']) + + def test_nginx_http_auth_fail(self): + response = self.app.get( + '/auth', headers={ + 'Auth-User': 'user', + 'Auth-Pass': 'bad password', + 'Client-IP': '127.0.0.1', + 'Auth-Protocol': 'imap', + 'Auth-Login-Attempt': '1', + }) + self.assertEquals(200, response.status_code) + self.assertNotEquals('OK', response.headers['Auth-Status']) +