1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-24 21:34:41 +03:00

Added "not tested" generic SMS sending using an HTTP server

This commit is contained in:
Adolfo Gómez García 2022-06-28 14:46:18 +02:00
parent 77e021a371
commit aec2f5b57f
8 changed files with 197 additions and 15 deletions

View File

@ -768,7 +768,7 @@ class gui:
def __init__(self, **options):
super().__init__(**options)
if options.get('values') and isinstance(options.get('values'), dict):
if options.get('values') and isinstance(options.get('values'), (dict, list, tuple)):
options['values'] = gui.convertToChoices(options['values'])
self._data['values'] = options.get('values', [])
if 'fills' in options:

View File

@ -5,7 +5,7 @@ import ssl
import typing
import logging
from django.utils.translation import ugettext_noop as _
from django.utils.translation import gettext_noop as _
from uds.core import mfas
from uds.core.ui import gui

View File

@ -0,0 +1 @@
from . import mfa

View File

@ -0,0 +1,179 @@
import typing
import logging
from django.utils.translation import gettext_noop as _, gettext
import requests
import requests.auth
from uds.core import mfas
from uds.core.ui import gui
if typing.TYPE_CHECKING:
from uds.core.module import Module
logger = logging.getLogger(__name__)
class SMSMFA(mfas.MFA):
typeName = _('SMS Thought HTTP')
typeType = 'smsHttpMFA'
typeDescription = _('Simple SMS sending MFA using HTTP')
iconFile = 'sms.png'
smsSendingUrl = gui.TextField(
length=128,
label=_('URL pattern for SMS sending'),
order=1,
tooltip=_(
'URL pattern for SMS sending. It can contain the following '
'variables:<br>'
'<ul>'
'<li>{code} - the code to send</li>'
'<li>{phone} - the phone number</li>'
'</ul>'
),
required=True,
tab=_('HTTP Server'),
)
ignoreCertificateErrors = gui.CheckBoxField(
label=_('Ignore certificate errors'),
order=2,
tab=_('HTTP Server'),
defvalue=False,
tooltip=_(
'If checked, the server certificate will be ignored. This is '
'useful if the server uses a self-signed certificate.'
),
)
smsSendingMethod = gui.ChoiceField(
label=_('SMS sending method'),
order=2,
tooltip=_('Method for sending SMS'),
required=True,
tab=_('HTTP Server'),
values=('GET', 'POST', 'PUT'),
)
smsSendingParameters = gui.TextField(
length=128,
label=_('Parameters for SMS POST/PUT sending'),
order=3,
tooltip=_(
'Parameters for SMS sending via POST/PUT. It can contain the following '
'variables:<br>'
'<ul>'
'<li>{code} - the code to send</li>'
'<li>{phone} - the phone number</li>'
'</ul>'
),
required=False,
tab=_('HTTP Server'),
)
smsAuthenticationMethod = gui.ChoiceField(
label=_('SMS authentication method'),
order=3,
tooltip=_('Method for sending SMS'),
required=True,
tab=_('HTTP Server'),
values=[
{'id': 0, 'text': _('None')},
{'id': 1, 'text': _('HTTP Basic Auth')},
{'id': 2, 'text': _('HTTP Digest Auth')},
{'id': 3, 'text': _('HTTP Token Auth')},
],
)
smsAuthenticationUserOrToken = gui.TextField(
length=128,
label=_('SMS authentication user or token'),
order=4,
tooltip=_('User or token for SMS authentication'),
required=False,
tab=_('HTTP Server'),
)
smsAuthenticationPassword = gui.TextField(
length=128,
label=_('SMS authentication password'),
order=5,
tooltip=_('Password for SMS authentication'),
required=False,
tab=_('HTTP Server'),
)
def initialize(self, values: 'Module.ValuesType') -> None:
return super().initialize(values)
def composeSmsUrl(self, code: str, phone: str) -> str:
url = self.smsSendingUrl.value
url = url.replace('{code}', code)
url = url.replace('{phone}', phone)
return url
def getSession(self) -> requests.Session:
session = requests.Session()
# 0 means no authentication
if self.smsAuthenticationMethod.value == 1:
session.auth = requests.auth.HTTPBasicAuth(
username=self.smsAuthenticationUserOrToken.value,
password=self.smsAuthenticationPassword.value,
)
elif self.smsAuthenticationMethod.value == 2:
session.auth = requests.auth.HTTPDigestAuth(
self.smsAuthenticationUserOrToken.value,
self.smsAuthenticationPassword.value,
)
elif self.smsAuthenticationMethod.value == 3:
session.headers['Authorization'] = (
'Token ' + self.smsAuthenticationUserOrToken.value
)
# Any other value means no authentication
return session
def sendSMS_GET(self, url: str) -> None:
response = self.getSession().get(url)
if response.status_code != 200:
raise Exception('Error sending SMS: ' + response.text)
def sendSMS_POST(self, url: str, code: str, phone: str) -> None:
# Compose POST data
data = ''
if self.smsSendingParameters.value:
data = self.smsSendingParameters.value.replace('{code}', code).replace(
'{phone}', phone
)
response = self.getSession().post(url, data=data.encode())
if response.status_code != 200:
raise Exception('Error sending SMS: ' + response.text)
def sendSMS_PUT(self, url: str, code: str, phone: str) -> None:
# Compose POST data
data = ''
if self.smsSendingParameters.value:
data = self.smsSendingParameters.value.replace('{code}', code).replace(
'{phone}', phone
)
response = self.getSession().put(url, data=data.encode())
if response.status_code != 200:
raise Exception('Error sending SMS: ' + response.text)
def sendSMS(self, code: str, phone: str) -> None:
url = self.composeSmsUrl(code, phone)
if self.smsSendingMethod.value == 'GET':
return self.sendSMS_GET(url)
elif self.smsSendingMethod.value == 'POST':
return self.sendSMS_POST(url, code, phone)
elif self.smsSendingMethod.value == 'PUT':
return self.sendSMS_PUT(url, code, phone)
else:
raise Exception('Unknown SMS sending method')
def label(self) -> str:
return gettext('MFA Code')
def sendCode(self, userId: str, identifier: str, code: str) -> None:
logger.debug('Sending code: %s', code)
return

BIN
server/src/uds/mfas/SMS/sms.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,7 +1,7 @@
import typing
import logging
from django.utils.translation import ugettext_noop as _
from django.utils.translation import gettext_noop as _
from uds.core import mfas
from uds.core.ui import gui

View File

@ -30,8 +30,9 @@
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django import forms
from uds.models import Authenticator
@ -69,4 +70,5 @@ class LoginForm(forms.Form):
continue
choices.append((a.uuid, a.name))
self.fields['authenticator'].choices = choices # type: ignore
typing.cast(forms.ChoiceField, self.fields['authenticator']).choices = choices

View File

@ -55,6 +55,8 @@ from uds.web.util import configjs
logger = logging.getLogger(__name__)
CSRF_FIELD = 'csrfmiddlewaretoken'
MFA_COOKIE_NAME = 'mfa_status'
if typing.TYPE_CHECKING:
from uds import models
@ -116,10 +118,9 @@ def login(
request.authorized = True
if user.manager.getType().providesMfa() and user.manager.mfa:
authInstance = user.manager.getInstance()
if authInstance.mfaIdentifier():
request.authorized = (
False # We can ask for MFA so first disauthorize user
)
if authInstance.mfaIdentifier(user.name):
# We can ask for MFA so first disauthorize user
request.authorized = False
response = HttpResponseRedirect(reverse('page.mfa'))
else:
@ -182,11 +183,10 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
userHashValue: str = hashlib.sha3_256(
(request.user.name + request.user.uuid + mfaProvider.uuid).encode()
).hexdigest()
cookieName = 'bgd' + userHashValue
# Try to get cookie anc check it
mfaCookie = request.COOKIES.get(cookieName, None)
if mfaCookie: # Cookie is valid, skip MFA setting authorization
mfaCookie = request.COOKIES.get(MFA_COOKIE_NAME, None)
if mfaCookie == userHashValue: # Cookie is valid, skip MFA setting authorization
request.authorized = True
return HttpResponseRedirect(reverse('page.index'))
@ -203,7 +203,7 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
request.session.flush() # Clear session, and redirect to login
return HttpResponseRedirect(reverse('page.login'))
mfaIdentifier = authInstance.mfaIdentifier()
mfaIdentifier = authInstance.mfaIdentifier(request.user.name)
label = mfaInstance.label()
if request.method == 'POST': # User has provided MFA code
@ -226,8 +226,8 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
and form.cleaned_data['remember'] is True
):
response.set_cookie(
cookieName,
'true',
MFA_COOKIE_NAME,
userHashValue,
max_age=mfaProvider.remember_device * 60 * 60,
)