issue #1095. Added two-factor authentication methods and onvalidation. Fixed last attempt two-factor retry login

issue #1095. Added return user for two_factor_onvalidation
This commit is contained in:
engeens
2015-10-29 18:49:01 +01:00
parent 4de007a946
commit 5c9d197f93

View File

@@ -1153,6 +1153,8 @@ class Auth(object):
manager_actions={},
auth_manager_role=None,
two_factor_authentication_group = None,
auth_two_factor_enabled = False,
auth_two_factor_tries_left = 3,
login_captcha=None,
register_captcha=None,
pre_registration_div=None,
@@ -1234,6 +1236,7 @@ class Auth(object):
invalid_login='Invalid login',
invalid_user='Invalid user',
invalid_password='Invalid password',
invalid_two_factor_code = 'Incorrect code. {0} more attempt(s) remaining.',
is_empty="Cannot be empty",
mismatched_password="Password fields don't match",
verify_email='Welcome %(username)s! Click on the link %(link)s to verify your email',
@@ -1247,6 +1250,8 @@ class Auth(object):
retrieve_password_subject='Password retrieve',
reset_password='Click on the link %(link)s to reset your password',
reset_password_subject='Password reset',
retrieve_two_factor_code='Your temporary login code is {0}',
retrieve_two_factor_code_subject='Two-step Login Authentication Code',
bulk_invite_subject='Invitation to join%(site)s',
bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process',
invalid_reset_password='Invalid reset password',
@@ -1292,6 +1297,8 @@ class Auth(object):
label_client_ip='Client IP',
label_origin='Origin',
label_remember_me="Remember me (for 30 days)",
label_two_factor='Authentication code',
two_factor_comment = 'This code was emailed to you and is required for login.',
verify_password_comment='please input your password again',
)
@@ -1508,7 +1515,9 @@ class Auth(object):
reset_password_onaccept=[],
hmac_key=hmac_key,
formstyle=current.response.formstyle,
label_separator=current.response.form_label_separator
label_separator=current.response.form_label_separator,
two_factor_methods = [],
two_factor_onvalidation = [],
)
settings.lock_keys = True
# ## these are messages that can be customized
@@ -2477,8 +2486,8 @@ class Auth(object):
session.auth_two_factor_user = None
session.auth_two_factor = None
session.auth_two_factor_enabled = False
# Allow up to 4 attempts (the 1st one plus 3 more)
session.auth_two_factor_tries_left = 3
# Set the number of attempts. It should be more than 1.
session.auth_two_factor_tries_left = self.settings.auth_two_factor_tries_left
def when_is_logged_in_bypass_next_in_url(self, next, session):
"""
@@ -2718,17 +2727,21 @@ class Auth(object):
# 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
# auth.settings.two_factor_authentication_group
if user and self.settings.two_factor_authentication_group:
# If auth.settings.auth_two_factor_enabled it will enable two factor
# for all the app. Another way to anble two factor is that the user
# must be part of a group that is called auth.settings.two_factor_authentication_group
if user and self.settings.auth_two_factor_enabled == True:
session.auth_two_factor_enabled = True
elif user and self.settings.two_factor_authentication_group:
role = self.settings.two_factor_authentication_group
session.auth_two_factor_enabled = self.has_membership(user_id=user.id, role=role)
# challenge
if session.auth_two_factor_enabled:
form = SQLFORM.factory(
Field('authentication_code',
label=self.messages.label_two_factor,
required=True,
comment='This code was emailed to you and is required for login.'),
comment=self.messages.two_factor_comment),
hidden=dict(_next=next),
formstyle=settings.formstyle,
separator=settings.label_separator
@@ -2743,17 +2756,81 @@ class Auth(object):
if session.auth_two_factor_user is None and user is not None:
session.auth_two_factor_user = user # store the validated user and associate with this session
session.auth_two_factor = random.randint(100000, 999999)
session.auth_two_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="Two-step Login Authentication Code",
message="Your temporary login code is {0}".format(session.auth_two_factor))
session.auth_two_factor_tries_left = self.settings.auth_two_factor_tries_left
# Set the way we generate the code or we send the code. For example using SMS...
two_factor_methods = self.settings.two_factor_methods
if two_factor_methods == []:
# TODO: Add some error checking to handle cases where email cannot be sent
self.settings.mailer.send(
to=user.email,
subject=self.messages.retrieve_two_factor_code_subject,
message=self.messages.retrieve_two_factor_code.format(session.auth_two_factor))
else:
#Check for all method. It is possible to have multiples
for two_factor_method in two_factor_methods:
try:
# By default we use session.auth_two_factor generated before.
session.auth_two_factor = two_factor_method(user, session.auth_two_factor)
except:
pass
else:
break
if form.accepts(request, session if self.csrf_prevention else None,
formname='login', dbio=False,
onvalidation=onvalidation,
hideerror=settings.hideerror):
accepted_form = True
accepted_form = True
'''
The lists is executed after form validation for each of the corresponding action.
For example, in your model:
In your models copy and paste:
#Before define tables, we add some extra field to auth_user
auth.settings.extra_fields['auth_user'] = [
Field('motp_secret', 'password', length=512, default='', label='MOTP Secret'),
Field('motp_pin', 'string', length=128, default='', label='MOTP PIN')]
OFFSET = 60 #Be sure is the same in your OTP Client
#Set session.auth_two_factor to None. Because the code is generated by external app.
# This will avoid to use the default setting and send a code by email.
def _set_two_factor(user, auth_two_factor):
return None
def verify_otp(user, otp):
import time
from hashlib import md5
epoch_time = int(time.time())
time_start = int(str(epoch_time - OFFSET)[:-1])
time_end = int(str(epoch_time + OFFSET)[:-1])
for t in range(time_start - 1, time_end + 1):
to_hash = str(t) + user.motp_secret + user.motp_pin
hash = md5(to_hash).hexdigest()[:6]
if otp == hash:
return hash
auth.settings.auth_two_factor_enabled = True
auth.messages.two_factor_comment = "Verify your OTP Client for the code."
auth.settings.two_factor_methods = [lambda user, auth_two_factor: _set_two_factor(user, auth_two_factor)]
auth.settings.two_factor_onvalidation = [lambda user, otp: verify_otp(user, otp)]
'''
if self.settings.two_factor_onvalidation != []:
for two_factor_onvalidation in self.settings.two_factor_onvalidation:
try:
session.auth_two_factor = two_factor_onvalidation(session.auth_two_factor_user, form.vars['authentication_code'])
except:
pass
else:
break
if form.vars['authentication_code'] == str(session.auth_two_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
@@ -2774,10 +2851,20 @@ class Auth(object):
# session usernamem will still exist
self._reset_two_factor_auth(session)
else:
# TODO: Limit the number of retries allowed.
response.flash = \
'Incorrect code. {0} more attempt(s) remaining.'.format(session.auth_two_factor_tries_left)
session.auth_two_factor_tries_left -= 1
# If the number of retries are higher than auth_two_factor_tries_left
# Require user to enter username and password again.
if session.auth_two_factor_enabled and session.auth_two_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_two_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)
response.flash = self.messages.invalid_two_factor_code.format(session.auth_two_factor_tries_left)
return form
else:
return form