mirror of
https://github.com/dkmstr/openuds.git
synced 2024-12-25 23:21:41 +03:00
Tested correct working of generic SMS sending using HTTP
This commit is contained in:
parent
76e67b1f63
commit
11d9c77a79
@ -49,7 +49,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class MFA(ModelHandler):
|
class MFA(ModelHandler):
|
||||||
model = models.MFA
|
model = models.MFA
|
||||||
save_fields = ['name', 'comments', 'tags', 'remember_device']
|
save_fields = ['name', 'comments', 'tags', 'remember_device', 'validity']
|
||||||
|
|
||||||
table_title = _('Multi Factor Authentication')
|
table_title = _('Multi Factor Authentication')
|
||||||
table_fields = [
|
table_fields = [
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import random
|
import random
|
||||||
|
import enum
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
@ -83,6 +84,13 @@ class MFA(Module):
|
|||||||
# : override it in your own implementation.
|
# : override it in your own implementation.
|
||||||
cacheTime: typing.ClassVar[int] = 5
|
cacheTime: typing.ClassVar[int] = 5
|
||||||
|
|
||||||
|
class RESULT(enum.Enum):
|
||||||
|
"""
|
||||||
|
This enum is used to know if the MFA code was sent or not.
|
||||||
|
"""
|
||||||
|
OK = 1
|
||||||
|
ALLOWED = 2
|
||||||
|
|
||||||
def __init__(self, environment: 'Environment', values: Module.ValuesType):
|
def __init__(self, environment: 'Environment', values: Module.ValuesType):
|
||||||
super().__init__(environment, values)
|
super().__init__(environment, values)
|
||||||
self.initialize(values)
|
self.initialize(values)
|
||||||
@ -118,24 +126,30 @@ class MFA(Module):
|
|||||||
"""
|
"""
|
||||||
return self.cacheTime
|
return self.cacheTime
|
||||||
|
|
||||||
def sendCode(self, userId: str, username: str, identifier: str, code: str) -> bool:
|
def emptyIndentifierAllowedToLogin(self) -> bool:
|
||||||
|
"""
|
||||||
|
If this method returns True, an user that has no "identifier" is allowed to login without MFA
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def sendCode(self, userId: str, username: str, identifier: str, code: str) -> 'MFA.RESULT':
|
||||||
"""
|
"""
|
||||||
This method will be invoked from "process" method, to send the MFA code to the user.
|
This method will be invoked from "process" method, to send the MFA code to the user.
|
||||||
If returns True, the MFA code was sent.
|
If returns MFA.RESULT.VALID, the MFA code was sent.
|
||||||
If returns False, the MFA code was not sent, the user does not need to enter the MFA code.
|
If returns MFA.RESULT.ALLOW, the MFA code was not sent, the user does not need to enter the MFA code.
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raise NotImplementedError('sendCode method not implemented')
|
raise NotImplementedError('sendCode method not implemented')
|
||||||
|
|
||||||
def process(self, userId: str, username: str, identifier: str, validity: typing.Optional[int] = None) -> bool:
|
def process(self, userId: str, username: str, identifier: str, validity: typing.Optional[int] = None) -> 'MFA.RESULT':
|
||||||
"""
|
"""
|
||||||
This method will be invoked from the MFA form, to send the MFA code to the user.
|
This method will be invoked from the MFA form, to send the MFA code to the user.
|
||||||
The identifier where to send the code, will be obtained from "mfaIdentifier" method.
|
The identifier where to send the code, will be obtained from "mfaIdentifier" method.
|
||||||
Default implementation generates a random code and sends invokes "sendCode" method.
|
Default implementation generates a random code and sends invokes "sendCode" method.
|
||||||
|
|
||||||
If returns True, the MFA code was sent.
|
If returns MFA.RESULT.VALID, the MFA code was sent.
|
||||||
If returns False, the MFA code was not sent, the user does not need to enter the MFA code.
|
If returns MFA.RESULT.ALLOW, the MFA code was not sent, the user does not need to enter the MFA code.
|
||||||
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
|
||||||
@ -146,7 +160,7 @@ class MFA(Module):
|
|||||||
# if we have a stored code, check if it's still valid
|
# if we have a stored code, check if it's still valid
|
||||||
if data[0] + datetime.timedelta(seconds=validity) < getSqlDatetime():
|
if data[0] + datetime.timedelta(seconds=validity) < getSqlDatetime():
|
||||||
# if it's still valid, just return without sending a new one
|
# if it's still valid, just return without sending a new one
|
||||||
return True
|
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(userId)
|
||||||
|
@ -148,9 +148,9 @@ class EmailMFA(mfas.MFA):
|
|||||||
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) -> bool:
|
def sendCode(self, userId: str, username: str, identifier: str, code: str) -> mfas.MFA.RESULT:
|
||||||
self.doSendCode(identifier, code)
|
self.doSendCode(identifier, code)
|
||||||
return True
|
return mfas.MFA.RESULT.OK
|
||||||
|
|
||||||
def login(self) -> smtplib.SMTP:
|
def login(self) -> smtplib.SMTP:
|
||||||
"""
|
"""
|
||||||
|
@ -67,6 +67,7 @@ class SMSMFA(mfas.MFA):
|
|||||||
'* {code} - the code to send\n'
|
'* {code} - the code to send\n'
|
||||||
'* {phone/+phone} - the phone number\n'
|
'* {phone/+phone} - the phone number\n'
|
||||||
'* {username} - the username\n'
|
'* {username} - the username\n'
|
||||||
|
'* {justUsername} - the username without @....\n'
|
||||||
'Headers are in the form of "Header: Value". (without the quotes)'
|
'Headers are in the form of "Header: Value". (without the quotes)'
|
||||||
),
|
),
|
||||||
required=False,
|
required=False,
|
||||||
@ -84,6 +85,7 @@ class SMSMFA(mfas.MFA):
|
|||||||
'* {code} - the code to send\n'
|
'* {code} - the code to send\n'
|
||||||
'* {phone/+phone} - the phone number\n'
|
'* {phone/+phone} - the phone number\n'
|
||||||
'* {username} - the username\n'
|
'* {username} - the username\n'
|
||||||
|
'* {justUsername} - the username without @....\n'
|
||||||
),
|
),
|
||||||
required=False,
|
required=False,
|
||||||
tab=_('HTTP Server'),
|
tab=_('HTTP Server'),
|
||||||
@ -121,7 +123,7 @@ class SMSMFA(mfas.MFA):
|
|||||||
tab=_('HTTP Authentication'),
|
tab=_('HTTP Authentication'),
|
||||||
)
|
)
|
||||||
|
|
||||||
smsAuthenticationPassword = gui.TextField(
|
smsAuthenticationPassword = gui.PasswordField(
|
||||||
length=256,
|
length=256,
|
||||||
label=_('SMS authentication password'),
|
label=_('SMS authentication password'),
|
||||||
order=22,
|
order=22,
|
||||||
@ -163,6 +165,7 @@ class SMSMFA(mfas.MFA):
|
|||||||
url = url.replace('{phone}', phone.replace('+', ''))
|
url = url.replace('{phone}', phone.replace('+', ''))
|
||||||
url = url.replace('{+phone}', phone)
|
url = url.replace('{+phone}', phone)
|
||||||
url = url.replace('{username}', userName)
|
url = url.replace('{username}', userName)
|
||||||
|
url = url.replace('{justUsername}', userName.split('@')[0])
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def getSession(self) -> requests.Session:
|
def getSession(self) -> requests.Session:
|
||||||
@ -188,59 +191,62 @@ class SMSMFA(mfas.MFA):
|
|||||||
session.headers[headerName.strip()] = headerValue.strip()
|
session.headers[headerName.strip()] = headerValue.strip()
|
||||||
return session
|
return session
|
||||||
|
|
||||||
def processResponse(self, response: requests.Response) -> None:
|
def processResponse(self, response: requests.Response) -> mfas.MFA.RESULT:
|
||||||
|
logger.debug('Response: %s', response)
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
if self.smsResponseErrorAction.value == '1':
|
if self.smsResponseErrorAction.value == '1':
|
||||||
raise Exception(_('SMS sending failed'))
|
raise Exception(_('SMS sending failed'))
|
||||||
else:
|
elif self.smsResponseOkRegex.value.strip():
|
||||||
return
|
logger.debug('Checking response OK regex: %s: (%s)', self.smsResponseOkRegex.value, re.search(self.smsResponseOkRegex.value, response.text))
|
||||||
|
if not re.search(self.smsResponseOkRegex.value, response.text or ''):
|
||||||
if self.smsResponseOkRegex.value.strip():
|
|
||||||
if not re.search(self.smsResponseOkRegex.value, response.text):
|
|
||||||
logger.error(
|
logger.error(
|
||||||
'SMS response error: %s',
|
'SMS response error: %s',
|
||||||
response.text,
|
response.text,
|
||||||
)
|
)
|
||||||
if self.smsResponseErrorAction.value == '1':
|
if self.smsResponseErrorAction.value == '1':
|
||||||
raise Exception('SMS response error')
|
raise Exception('SMS response error')
|
||||||
|
return mfas.MFA.RESULT.ALLOWED
|
||||||
|
return mfas.MFA.RESULT.OK
|
||||||
|
|
||||||
def sendSMS_GET(self, userId: str, username: str, url: str) -> None:
|
def sendSMS_GET(self, userId: str, username: str, url: str) -> mfas.MFA.RESULT:
|
||||||
self.processResponse(self.getSession().get(url))
|
return self.processResponse(self.getSession().get(url))
|
||||||
|
|
||||||
def sendSMS_POST(
|
def getData(
|
||||||
self, userId: str, username: str, url: str, code: str, phone: str
|
self, userId: str, username: str, url: str, code: str, phone: str
|
||||||
) -> None:
|
) -> bytes:
|
||||||
# Compose POST data
|
|
||||||
data = ''
|
|
||||||
if self.smsSendingParameters.value:
|
|
||||||
data = self.smsSendingParameters.value.replace('{code}', code).replace(
|
|
||||||
'{phone}',
|
|
||||||
phone.replace('+', '')
|
|
||||||
.replace('{+phone}', phone)
|
|
||||||
.replace('{username}', username),
|
|
||||||
)
|
|
||||||
bdata = data.encode(self.smsEncoding.value)
|
|
||||||
session = self.getSession()
|
|
||||||
# Add content-length header
|
|
||||||
session.headers['Content-Length'] = str(len(bdata))
|
|
||||||
|
|
||||||
self.processResponse(session.post(url, data=bdata))
|
|
||||||
|
|
||||||
def sendSMS_PUT(
|
|
||||||
self, userId: str, username: str, url: str, code: str, phone: str
|
|
||||||
) -> None:
|
|
||||||
# Compose POST data
|
|
||||||
data = ''
|
data = ''
|
||||||
if self.smsSendingParameters.value:
|
if self.smsSendingParameters.value:
|
||||||
data = (
|
data = (
|
||||||
self.smsSendingParameters.value.replace('{code}', code)
|
self.smsSendingParameters.value.replace('{code}', code)
|
||||||
.replace('{phone}', phone)
|
.replace('{phone}', phone.replace('+', ''))
|
||||||
.replace('{+phone}', phone)
|
.replace('{+phone}', phone)
|
||||||
.replace('{username}', username)
|
.replace('{username}', username)
|
||||||
|
.replace('{justUsername}', username.split('@')[0])
|
||||||
)
|
)
|
||||||
self.processResponse(self.getSession().put(url, data=data.encode()))
|
return data.encode(self.smsEncoding.value)
|
||||||
|
|
||||||
def sendSMS(self, userId: str, username: str, code: str, phone: str) -> None:
|
def sendSMS_POST(
|
||||||
|
self, userId: str, username: str, url: str, code: str, phone: str
|
||||||
|
) -> mfas.MFA.RESULT:
|
||||||
|
# Compose POST data
|
||||||
|
session = self.getSession()
|
||||||
|
bdata = self.getData(userId, username, url, code, phone)
|
||||||
|
# Add content-length header
|
||||||
|
session.headers['Content-Length'] = str(len(bdata))
|
||||||
|
|
||||||
|
return self.processResponse(session.post(url, data=bdata))
|
||||||
|
|
||||||
|
def sendSMS_PUT(
|
||||||
|
self, userId: str, username: str, url: str, code: str, phone: str
|
||||||
|
) -> mfas.MFA.RESULT:
|
||||||
|
# Compose POST data
|
||||||
|
data = ''
|
||||||
|
bdata = self.getData(userId, username, url, code, phone)
|
||||||
|
return self.processResponse(self.getSession().put(url, data=bdata))
|
||||||
|
|
||||||
|
def sendSMS(
|
||||||
|
self, userId: str, username: str, code: str, phone: str
|
||||||
|
) -> mfas.MFA.RESULT:
|
||||||
url = self.composeSmsUrl(userId, username, code, phone)
|
url = self.composeSmsUrl(userId, username, code, phone)
|
||||||
if self.smsSendingMethod.value == 'GET':
|
if self.smsSendingMethod.value == 'GET':
|
||||||
return self.sendSMS_GET(userId, username, url)
|
return self.sendSMS_GET(userId, username, url)
|
||||||
@ -254,6 +260,14 @@ class SMSMFA(mfas.MFA):
|
|||||||
def label(self) -> str:
|
def label(self) -> str:
|
||||||
return gettext('MFA Code')
|
return gettext('MFA Code')
|
||||||
|
|
||||||
def sendCode(self, userId: str, username: str, identifier: str, code: str) -> bool:
|
def sendCode(
|
||||||
logger.debug('Sending code: %s', code)
|
self, userId: str, username: str, identifier: str, code: str
|
||||||
return True
|
) -> mfas.MFA.RESULT:
|
||||||
|
logger.debug(
|
||||||
|
'Sending SMS code "%s" for user %s (userId="%s", identifier="%s")',
|
||||||
|
code,
|
||||||
|
username,
|
||||||
|
userId,
|
||||||
|
identifier,
|
||||||
|
)
|
||||||
|
return self.sendSMS(userId, username, code, identifier)
|
||||||
|
@ -34,7 +34,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) -> bool:
|
def sendCode(self, userId: str, username: str, identifier: str, code: str) -> mfas.MFA.RESULT:
|
||||||
logger.debug('Sending code: %s', code)
|
logger.debug('Sending code: %s', code)
|
||||||
return True
|
return mfas.MFA.RESULT.OK
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@
|
|||||||
"""
|
"""
|
||||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||||
"""
|
"""
|
||||||
import datetime
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
import hashlib
|
||||||
@ -44,6 +43,7 @@ from django.utils.translation import gettext as _
|
|||||||
from uds.core.util.request import ExtendedHttpRequest, ExtendedHttpRequestWithUser
|
from uds.core.util.request import ExtendedHttpRequest, ExtendedHttpRequestWithUser
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
|
|
||||||
|
from uds.core import mfas
|
||||||
from uds.core.auths import auth, exceptions
|
from uds.core.auths import auth, exceptions
|
||||||
from uds.web.util import errors
|
from uds.web.util import errors
|
||||||
from uds.web.forms.LoginForm import LoginForm
|
from uds.web.forms.LoginForm import LoginForm
|
||||||
@ -118,8 +118,6 @@ def login(
|
|||||||
request.authorized = True
|
request.authorized = True
|
||||||
if user.manager.getType().providesMfa() and user.manager.mfa:
|
if user.manager.getType().providesMfa() and user.manager.mfa:
|
||||||
authInstance = user.manager.getInstance()
|
authInstance = user.manager.getInstance()
|
||||||
if authInstance.mfaIdentifier(user.name):
|
|
||||||
# We can ask for MFA so first disauthorize user
|
|
||||||
request.authorized = False
|
request.authorized = False
|
||||||
response = HttpResponseRedirect(reverse('page.mfa'))
|
response = HttpResponseRedirect(reverse('page.mfa'))
|
||||||
|
|
||||||
@ -206,6 +204,15 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
|
|||||||
mfaIdentifier = authInstance.mfaIdentifier(request.user.name)
|
mfaIdentifier = authInstance.mfaIdentifier(request.user.name)
|
||||||
label = mfaInstance.label()
|
label = mfaInstance.label()
|
||||||
|
|
||||||
|
if not mfaIdentifier:
|
||||||
|
if mfaInstance.emptyIndentifierAllowedToLogin():
|
||||||
|
# Allow login
|
||||||
|
request.authorized = True
|
||||||
|
return HttpResponseRedirect(reverse('page.index'))
|
||||||
|
# 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)
|
||||||
|
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
|
||||||
form = MFAForm(request.POST)
|
form = MFAForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@ -241,7 +248,7 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
|
|||||||
# 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(userHashValue, request.user.name, mfaIdentifier, validity=validity)
|
||||||
if not result:
|
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
|
||||||
return HttpResponseRedirect(reverse('page.index'))
|
return HttpResponseRedirect(reverse('page.index'))
|
||||||
@ -249,8 +256,8 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
|
|||||||
# store on session the start time of the MFA process if not already stored
|
# store on session the start time of the MFA process if not already stored
|
||||||
if 'mfa_start_time' not in request.session:
|
if 'mfa_start_time' not in request.session:
|
||||||
request.session['mfa_start_time'] = time.time()
|
request.session['mfa_start_time'] = time.time()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
logger.exception('Error processing MFA')
|
logger.error('Error processing MFA: %s', e)
|
||||||
return errors.errorView(request, errors.UNKNOWN_ERROR)
|
return errors.errorView(request, errors.UNKNOWN_ERROR)
|
||||||
|
|
||||||
# Compose a nice "XX years, XX months, XX days, XX hours, XX minutes" string from mfaProvider.remember_device
|
# Compose a nice "XX years, XX months, XX days, XX hours, XX minutes" string from mfaProvider.remember_device
|
||||||
|
Loading…
Reference in New Issue
Block a user