Failed login attempt lockout

482 views
Skip to first unread message

LaDarrius Stewart

unread,
Sep 18, 2014, 4:13:40 PM9/18/14
to web...@googlegroups.com
Needed a way to lock a user account on three failed attempts. I started off by doing the following...

def checkuser():
    a = request.vars.username
    b = db((db.auth_user.username==str(a))).select().first()
    if b is not None:
        b.update_record(Attempts=b.Attempts + 1)
        
def loginonval(form):
    form = form
    a = form.vars.username
    b = db((db.auth_user.username==str(a))).select().first().Attempts
    session.leetspeak = 1337
    if b >2:
        redirect(URL('default','accountlock'))

auth.settings.login_onfail = checkuser()
auth.settings.login_onvalidation = [lambda form: loginonval(form)]

Also onaccept callback for login I update the value back to 0. My issue is finding out where the logic for locking someone out should be placed. The above works fine I guess but I'm wondering if this is best practice.
Secondly I was shocked this feature didn't already exist within web2py which lead me to believe that 9/10 there is a security reason. Are there any concerns that I should be worried about?

This message is for named person(s) only.  It may contain confidential and/or legally privileged information.  No confidentiality or privilege is waived or lost should mis-transmission occur.  If you receive this message in error, delete it (and all copies) and notify the sender.  You must not, directly or indirectly,use, disclose, distribute, print, or copy any part of this message if you are not the intended recipient. GAD GROUP TECHNOLOGY, INC. reserves the right to monitor all e-mail communications through its networks.

Any views expressed in this message are those of the individual sender, except where the message states otherwise and the sender is authorized to state them to be the views of any such entity.

This e-mail has been virus and content scanned by GAD GROUP TECHNOLOGY, INC.

Anthony

unread,
Sep 18, 2014, 6:11:38 PM9/18/14
to
def checkuser():
    a = request.vars.username
    b = db((db.auth_user.username==str(a))).select().first()
    if b is not None:
        b.update_record(Attempts=b.Attempts + 1)

Note, you don't need to bother first checking for the record, so you can save a query by just doing the update:

auth.settings.login_onfail = lambda: db(db.auth_user.username == request.vars.username).update(
   
Attempts=db.auth_user.Attempts + 1)
 
auth.settings.login_onvalidation = [lambda form: loginonval(form)]

Note, if you've already got a function that takes the correct set of arguments, you don't need to wrap it in a lambda -- just do:

auth.settings.login_onvalidation = [loginonval]

Also, rather than a complete lockout, you might consider locking out for a limited time (e.g., 15 or 30 minutes).

Anthony

Leonel Câmara

unread,
Sep 18, 2014, 6:30:36 PM9/18/14
to web...@googlegroups.com
This last suggestion by Anthony of limiting to 15-30 minutes is also easy enough to do if you store the attempt numbers in cache then you can use cache expiration time to limit its effects.

黄祥

unread,
Apr 22, 2015, 7:28:22 AM4/22/15
to web...@googlegroups.com
i'm tried to do the same thing, but with no luck
e.g.
def login_attempts():
a = request.vars.username
b = db((db.auth_user.username == str(a))).select().first()
#login_attempts = 0
if b is not None:
#login_attempts += 1
session.login_attempts = (session.login_attempts or 0) + 1
#if login_attempts >= 3 :
#cache.ram('message', lambda: login_attempts, time_expire = 5)
#response.flash = login_attempts
#session.flash = login_attempts
#redirect(URL('default','test'))
if session.login_attempts >= 3 :
login_attempts = cache.ram('login_attempts', lambda: login_attempts, time_expire = 5)
if not login_attempts:
session.forget(response)
#response.flash = session.login_attempts
#session.flash = session.login_attempts
#redirect(URL('default','test'))

auth.settings.login_onfail = login_attempts()

Error snapshot help

<type 'exceptions.NameError'>(free variable 'login_attempts' referenced before assignment in enclosing scope)


is there a way to have login attempt lockout, that save in cache?

thanks and best regards,
stifan

黄祥

unread,
Apr 22, 2015, 9:33:32 AM4/22/15
to web...@googlegroups.com
it seems that the refresh login page, is count as login_onfail and login_onvalidation in web2py default user login form.
e.g.
models/db.py
def login_attempts():
session.login_attempts = (session.login_attempts or 0) + 1
if session.login_attempts >= 3 :
#cache.ram('login_attempts', lambda: session.login_attempts, time_expire = 5)
response.flash = session.login_attempts

#auth.settings.login_onfail = login_attempts()
auth.settings.login_onvalidation = [login_attempts()]

views/default/user.html add response toolbar
{{=response.toolbar()}}

1. when i hit refresh https://127.0.0.1/test/default/user/login, the session is added by 1
2. when i try to input the wrong login, the sessions added by 2 (1 added by failure, n 1 added by refreshing the login form, i guess)

trying to use login_onfail and login_onvalidation got the same result.
is it normal behaviour, or i have the wrong steps?

Anthony

unread,
Apr 22, 2015, 9:51:57 AM4/22/15
to web...@googlegroups.com
You shouldn't be calling the callback function when setting the callback -- just put the function itself in the list -- web2py will call it at the appropriate point. Also, like the other Auth callback settings, login_onfail is a list, so you should append to it.

Instead of:

auth.settings.login_onfail = login_attempts()

it should be:

auth.settings.login_onfail.append(login_attempts)

Anthony

黄祥

unread,
Apr 22, 2015, 11:20:37 AM4/22/15
to web...@googlegroups.com
thank you so much, anthony, the session is not counted anymore when refresh the login page.
i want to lock failed user login which tried 3 times, redirect to another pages for several times (5 sec in example below), after that time is fulfilled reset the counted login attempt. tried using variable but return an error (reference before assignment), tried using session (no error occured but the result is not expected (i think session has it's own expire time) ). how can i achieve it using web2py?
e.g.
"""
login_attempts = 1

def login_attempts(form):
#login_attempts = 1
if login_attempts >= 3 :
test = cache.ram('login_attempts', lambda: login_attempts, time_expire = 5)
if test:
redirect(URL('default', 'test') )
else:
login_attempts = 0
#response.flash = login_attempts
else :
login_attempts += 1
"""

def login_attempts(form):
session.login_attempts = (session.login_attempts or 0) + 1
if session.login_attempts >= 3 :
if cache.ram('login_attempts', lambda: session.login_attempts, time_expire = 5):
redirect(URL('default', 'test') )
else:
session.login_attempts = 0
#session.forget(response)

auth.settings.login_onfail.append(login_attempts)

thanks and best regards,
stifan

Alex Glaros

unread,
Apr 22, 2015, 12:54:21 PM4/22/15
to web...@googlegroups.com
when solved, can you please post the entire solution in an easy-to-copy format?

thanks

Alex Glaros

Anthony

unread,
Apr 22, 2015, 5:28:12 PM4/22/15
to web...@googlegroups.com
You can't simply reset a global variable as you are doing in the first example, as it will be reset on every request. Also, I'm not sure what you're trying to achieve with the cache example, but that code won't force a redirect for 5 seconds (and you don't want to have just a single cache key -- you would need one per user). Anyway, you shouldn't rely on the session, as a malicious user could simply start new sessions to keep making attempts. Instead, you would have to keep track of the user ID and the number of attempts via some other means (e.g., the database or the cache).

Anthony

黄祥

unread,
Apr 22, 2015, 9:52:34 PM4/22/15
to web...@googlegroups.com
pardon me, still not understood what do you mean with the cache. on my example above yet, i still not sure which one to use, yet your hints, quite clear about database. thank you anthony.
e.g. work fine
models/db.py
auth = Auth(db)

auth.settings.extra_fields['auth_user']= [
  Field('Attempts', 'integer') ]

auth.define_tables(username=True, signature=False)

def login_attempts(form):
username = request.vars.username
row = db((db.auth_user.username == username ) ).select().first()
if row is not None:
db(db.auth_user.id == row.id).update(Attempts = row.Attempts + 1)
db.auth_event.insert(time_stamp = request.now, 
client_ip = request.client, 
user_id = row.id
origin = '%s/%s' % (request.controller, 
request.function), 
description = '%s login failed' % (row.username) )
if row.Attempts >= 3:
redirect(URL('default', 'test') )
else:
redirect(URL('default', 'user/login') )

auth.settings.login_onfail.append(login_attempts)

but when tried to combine with cache and banned ip it's not work (no errors occured but the result is not expected)
e.g. same code like above just a modification on if conditional
if row.Attempts >= 3:
#BAN_IP_TIME = 60 * 60 * 24 # 1 day
BAN_IP_TIME = 10
ban_key = request.client + 'ban'
if cache.ram(ban_key, lambda: False, BAN_IP_TIME):
raise HTTP(429, 'IP blocked')                                                                                           

# maximum number of fast requests allowed before banned
MAX_REQUESTS = 3 
request_key = request.client + 'requests'
cache.ram(request_key, lambda: 0, 1)
if cache.ram.increment(request_key) > MAX_REQUESTS:
cache.ram(ban_key, lambda: True, BAN_IP_TIME)
redirect(URL('default', 'test') )

Anthony

unread,
Apr 22, 2015, 10:31:57 PM4/22/15
to web...@googlegroups.com
Why are you bothering with the cache given that you're already tracking login attempts in the auth_user table?

黄祥

unread,
Apr 22, 2015, 10:53:24 PM4/22/15
to web...@googlegroups.com
the idea is base on wordpress plugin 'limit login attempts', that i want to achieve it using web2py. 
first i want to start from simple, just record the attempted times in database table, 
after that, banned ip user for several time (minutes or hours) if the failed login is reached max retries times (e.g. 3 times), till the record in database is reseted by admin the user can try the login, during the ip banned, is redirect to another page.
wordpress limit login attempts.png
Message has been deleted

黄祥

unread,
Apr 22, 2015, 11:16:53 PM4/22/15
to web...@googlegroups.com
the idea is taken from wordpress plugin 'limit login attempts' that i want to achieve using web2py.
first, create record in database table, when user login failed.
after that, banned the ip address user for several time (e.g. 1 min) if the user attempt login is reached the limit (e.g. 3 times) during the banned period, user tried to access got redirect to another page. after the banned time is expired, the database reset the attempts record in database to 0, so that user can't be access the login page.
Reply all
Reply to author
Forward
0 new messages