Merge remote-tracking branch 'origin/v3.5-mfa'

This commit is contained in:
Adolfo Gómez García 2022-07-02 00:18:09 +02:00
commit 2736390f95
4 changed files with 38 additions and 18 deletions

View File

@ -154,7 +154,8 @@ class MFA(Module):
If raises an error, the MFA code was not sent, and the user needs to enter the MFA code. If raises an error, the MFA code was not sent, and the user needs to enter the MFA code.
""" """
# try to get the stored code # try to get the stored code
data: typing.Any = self.storage.getPickle(userId) storageKey = request.ip + userId
data: typing.Any = self.storage.getPickle(storageKey)
validity = validity if validity is not None else self.validity() * 60 validity = validity if validity is not None else self.validity() * 60
try: try:
if data and validity: if data and validity:
@ -164,18 +165,18 @@ class MFA(Module):
return MFA.RESULT.OK return MFA.RESULT.OK
except Exception: except Exception:
# if we have a problem, just remove the stored code # if we have a problem, just remove the stored code
self.storage.remove(userId) self.storage.remove(storageKey)
# Generate a 6 digit code (0-9) # Generate a 6 digit code (0-9)
code = ''.join(random.SystemRandom().choices('0123456789', k=6)) code = ''.join(random.SystemRandom().choices('0123456789', k=6))
logger.debug('Generated OTP is %s', code) logger.debug('Generated OTP is %s', code)
# Store the code in the database, own storage space # Store the code in the database, own storage space
self.storage.putPickle(userId, (getSqlDatetime(), code)) self.storage.putPickle(storageKey, (getSqlDatetime(), code))
# Send the code to the user # Send the code to the user
return self.sendCode(request, userId, username, identifier, code) return self.sendCode(request, userId, username, identifier, code)
def validate(self, userId: str, username: str, identifier: str, code: str, validity: typing.Optional[int] = None) -> None: def validate(self, request: 'ExtendedHttpRequest', userId: str, username: str, identifier: str, code: str, validity: typing.Optional[int] = None) -> None:
""" """
If this method is provided by an authenticator, the user will be allowed to enter a MFA code If this method is provided by an authenticator, the user will be allowed to enter a MFA code
You must raise an "exceptions.MFAError" if the code is not valid. You must raise an "exceptions.MFAError" if the code is not valid.
@ -184,7 +185,8 @@ class MFA(Module):
try: try:
err = _('Invalid MFA code') err = _('Invalid MFA code')
data = self.storage.getPickle(userId) storageKey = request.ip + userId
data = self.storage.getPickle(storageKey)
if data and len(data) == 2: if data and len(data) == 2:
validity = validity if validity is not None else self.validity() * 60 validity = validity if validity is not None else self.validity() * 60
if validity and data[0] + datetime.timedelta(seconds=validity) > getSqlDatetime(): if validity and data[0] + datetime.timedelta(seconds=validity) > getSqlDatetime():
@ -194,7 +196,7 @@ class MFA(Module):
# Check if the code is valid # Check if the code is valid
if data[1] == code: if data[1] == code:
# Code is valid, remove it from storage # Code is valid, remove it from storage
self.storage.remove(userId) self.storage.remove(storageKey)
return return
except Exception as e: except Exception as e:
# Any error means invalid code # Any error means invalid code

View File

@ -14,6 +14,7 @@ from uds.core.util import validators, decorators
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from uds.core.module import Module from uds.core.module import Module
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -128,7 +129,7 @@ class EmailMFA(mfas.MFA):
return 'OTP received via email' return 'OTP received via email'
@decorators.threaded @decorators.threaded
def doSendCode(self, identifier: str, code: str) -> None: def doSendCode(self, request: 'ExtendedHttpRequest', identifier: str, code: str) -> None:
# Send and email with the notification # Send and email with the notification
with self.login() as smtp: with self.login() as smtp:
try: try:
@ -138,18 +139,18 @@ class EmailMFA(mfas.MFA):
msg['From'] = self.fromEmail.cleanStr() msg['From'] = self.fromEmail.cleanStr()
msg['To'] = identifier msg['To'] = identifier
msg.attach(MIMEText(f'Your verification code is {code}', 'plain')) msg.attach(MIMEText(f'A login attemt has been made from {request.ip}.\nTo continue, provide the verification code {code}', 'plain'))
if self.enableHTML.value: if self.enableHTML.value:
msg.attach(MIMEText(f'<p>Your OTP code is <b>{code}</b></p>', 'html')) msg.attach(MIMEText(f'<p>A login attemt has been made from <b>{request.ip}</b>.</p><p>To continue, provide the verification code <b>{code}</b></p>', 'html'))
smtp.sendmail(self.fromEmail.value, identifier, msg.as_string()) smtp.sendmail(self.fromEmail.value, identifier, msg.as_string())
except smtplib.SMTPException as e: except smtplib.SMTPException as e:
logger.error('Error sending email: {}'.format(e)) logger.error('Error sending email: {}'.format(e))
raise raise
def sendCode(self, userId: str, username: str, identifier: str, code: str) -> mfas.MFA.RESULT: def sendCode(self, request: 'ExtendedHttpRequest', userId: str, username: str, identifier: str, code: str) -> mfas.MFA.RESULT:
self.doSendCode(identifier, code) self.doSendCode(request, identifier, code,)
return mfas.MFA.RESULT.OK return mfas.MFA.RESULT.OK
def login(self) -> smtplib.SMTP: def login(self) -> smtplib.SMTP:

View File

@ -9,6 +9,7 @@ from uds.core.ui import gui
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from uds.core.module import Module from uds.core.module import Module
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,7 +35,7 @@ class SampleMFA(mfas.MFA):
def label(self) -> str: def label(self) -> str:
return 'Code is in log' return 'Code is in log'
def sendCode(self, userId: str, username: str, identifier: str, code: str) -> mfas.MFA.RESULT: def sendCode(self, request: 'ExtendedHttpRequest', userId: str, username: str, identifier: str, code: str) -> mfas.MFA.RESULT:
logger.debug('Sending code: %s', code) logger.debug('Sending code: %s (from %s)', code, request.ip)
return mfas.MFA.RESULT.OK return mfas.MFA.RESULT.OK

View File

@ -208,7 +208,12 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
request.authorized = True request.authorized = True
return HttpResponseRedirect(reverse('page.index')) return HttpResponseRedirect(reverse('page.index'))
# Not allowed to login, redirect to login error page # Not allowed to login, redirect to login error page
logger.warning('MFA identifier not found for user %s on authenticator %s. It is required by MFA %s', request.user.name, request.user.manager.name, mfaProvider.name) logger.warning(
'MFA identifier not found for user %s on authenticator %s. It is required by MFA %s',
request.user.name,
request.user.manager.name,
mfaProvider.name,
)
return errors.errorView(request, errors.ACCESS_DENIED) return errors.errorView(request, errors.ACCESS_DENIED)
if request.method == 'POST': # User has provided MFA code if request.method == 'POST': # User has provided MFA code
@ -217,7 +222,12 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
code = form.cleaned_data['code'] code = form.cleaned_data['code']
try: try:
mfaInstance.validate( mfaInstance.validate(
userHashValue, request.user.name, mfaIdentifier, code, validity=validity request,
userHashValue,
request.user.name,
mfaIdentifier,
code,
validity=validity,
) )
request.authorized = True request.authorized = True
# Remove mfa_start_time from session # Remove mfa_start_time from session
@ -245,7 +255,13 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
else: else:
# Make MFA send a code # Make MFA send a code
try: try:
result = mfaInstance.process(userHashValue, request.user.name, mfaIdentifier, validity=validity) result = mfaInstance.process(
request,
userHashValue,
request.user.name,
mfaIdentifier,
validity=validity,
)
if result == mfas.MFA.RESULT.ALLOWED: if result == mfas.MFA.RESULT.ALLOWED:
# MFA not needed, redirect to index after authorization of the user # MFA not needed, redirect to index after authorization of the user
request.authorized = True request.authorized = True