Added support for two-step authentication. The login function will

now email a 6-digit code to users if the user is part of a group
with role 'web2py TWo-Step Authentication'.
This commit is contained in:
ortgit
2014-04-27 01:39:51 -04:00
parent 53ecc17b57
commit 87edbccf5b
2 changed files with 233 additions and 126 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<h2>{{=T( request.args(0).replace('_',' ').capitalize() )}}</h2> <h2>{{=T( request.args(0).replace('_',' ').capitalize() )}}</h2>
<div id="web2py_user_form"> <div id="web2py_user_form">
{{ {{
if request.args(0)=='login': if request.args(0)=='login' and not session.auth_2_factor_user:
if not 'register' in auth.settings.actions_disabled: if not 'register' in auth.settings.actions_disabled:
form.add_button(T('Register'),URL(args='register', vars={'_next': request.vars._next} if request.vars._next else None),_class='btn') form.add_button(T('Register'),URL(args='register', vars={'_next': request.vars._next} if request.vars._next else None),_class='btn')
pass pass
+109 -2
View File
@@ -2214,6 +2214,17 @@ class Auth(object):
_code='INVALID TICKET')) _code='INVALID TICKET'))
raise HTTP(200, message) raise HTTP(200, message)
def _reset_2_factor_auth(self, session):
'''When two-step authentication is enabled, this function is used to
clear the session after successfully completing second challenge
or when the maximum number of tries allowed has expired.
'''
session.auth_2_factor_user = None
session.auth_2_factor = None
session.auth_2_factor_enabled = False
# Allow up to 4 attempts (the 1st one plus 3 more)
session.auth_2_factor_tries_left = 3
def login( def login(
self, self,
next=DEFAULT, next=DEFAULT,
@@ -2293,6 +2304,28 @@ class Auth(object):
old_requires = table_user[username].requires old_requires = table_user[username].requires
table_user[username].requires = tmpvalidator table_user[username].requires = tmpvalidator
# If two-factor authentication is enabled, and the maximum
# number of tries allowed is used up, reset the session to
# pre-login state with two-factor auth
if session.auth_2_factor_enabled and session.auth_2_factor_tries_left < 1:
# Exceeded maximum allowed tries for this code. Require user to enter
# username and password again.
user = None
accepted_form = False
self._reset_2_factor_auth(session)
# Redirect to the default 'next' page without logging
# in. If that page requires login, user will be redirected
# back to the main login form
redirect(next, client_side=settings.client_side)
# Before showing the default login form, check whether
# we are already on the second step of two-step authentication.
# If we are, then skip this login form and use the form for the
# second challenge instead.
# Note to devs: The code inside the if-block is unchanged from the
# previous version of this file, other than for indentation inside
# to put it inside the if-block
if session.auth_2_factor_user is None:
# do we use our own login form, or from a central source? # do we use our own login form, or from a central source?
if settings.login_form == self: if settings.login_form == self:
form = SQLFORM( form = SQLFORM(
@@ -2414,8 +2447,7 @@ class Auth(object):
self.url(args=request.args, vars=request.get_vars), self.url(args=request.args, vars=request.get_vars),
client_side=settings.client_side) client_side=settings.client_side)
else: else: # use a central authentication server
# use a central authentication server
cas = settings.login_form cas = settings.login_form
cas_user = cas.get_user() cas_user = cas.get_user()
@@ -2432,6 +2464,77 @@ class Auth(object):
redirect(cas.login_url(next), redirect(cas.login_url(next),
client_side=settings.client_side) client_side=settings.client_side)
# Extra login logic for two-factor authentication
#################################################
# If the 'user' variable has a value, this means that the first
# authentication step was successful (i.e. user provided correct
# username and password at the first challenge).
# Check if this user is signed up for two-factor authentication
# Default rule is that the user must be part of a group that is called
# 'web2py Two-Step Authentication'
if user:
memberships = self.db(self.table_membership().user_id == user.id).select()
session.auth_2_factor_enabled = 'web2py Two-Step Authentication' in [i.group_id for i in memberships.values()]
# If user is signed up for two-factor authentication, present the second
# challenge
if session.auth_2_factor_enabled:
form = SQLFORM.factory(
Field('authentication_code',
required=True,
comment='This code was emailed to you and is required for login.'),
hidden=dict(_next=next),
formstyle=settings.formstyle,
separator=settings.label_separator
)
# accepted_form is used by some default web2py code later in the
# function that handles running specified functions before redirect
# Set it to False until the challenge form is accepted.
accepted_form = False
# Handle the case when a user has submitted the login/password
# form successfully, and the password has been validated, but
# the two-factor form has not been displayed or validated yet.
if session.auth_2_factor_user is None and user is not None:
session.auth_2_factor_user = user # store the validated user and associate with this session
session.auth_2_factor = random.randint(100000, 999999)
session.auth_2_factor_tries_left = 3 # Allow user to try up to 4 times
# TODO: Add some error checking to handle cases where email cannot be sent
self.settings.mailer.send(
to=user.email,
subject="FDSI Login Authentication Code",
message="Your temporary login code is {0}".format(session.auth_2_factor))
if form.accepts(request, session if self.csrf_prevention else None,
formname='login', dbio=False,
onvalidation=onvalidation,
hideerror=settings.hideerror):
accepted_form = True
if form.vars['authentication_code'] == str(session.auth_2_factor):
# Handle the case when the two-factor form has been successfully validated
# and the user was previously stored (the current user should be None because
# in this case, the previous username/password login form should not be displayed.
# This will allow the code after the 2-factor authentication block to proceed as
# normal.
if user is None or user == session.auth_2_factor_user:
user = session.auth_2_factor_user
# For security, because the username stored in the
# session somehow does not match the just validated
# user. Should not be possible without session stealing
# which is hard with SSL.
elif user != session.auth_2_factor_user:
user = None
# Either way, the user and code associated with this session should
# be removed. This handles cases where the session login may have
# expired but browser window is open, so the old session key and
# session usernamem will still exist
self._reset_2_factor_auth(session)
else:
# TODO: Limit the number of retries allowed.
response.flash = 'Incorrect code. {0} more attempt(s) remaining.'.format(session.auth_2_factor_tries_left)
session.auth_2_factor_tries_left -= 1
return form
else:
return form
# End login logic for two-factor authentication
# process authenticated users # process authenticated users
if user: if user:
user = Row(table_user._filter_fields(user, id=True)) user = Row(table_user._filter_fields(user, id=True))
@@ -2469,6 +2572,10 @@ class Auth(object):
Logouts and redirects to login Logouts and redirects to login
""" """
# Clear out 2-step authentication information if user logs
# out. This information is also cleared on successful login.
self._reset_2_factor_auth(current.session)
if next is DEFAULT: if next is DEFAULT:
next = self.get_vars_next() or self.settings.logout_next next = self.get_vars_next() or self.settings.logout_next
if onlogout is DEFAULT: if onlogout is DEFAULT: