mirror of
https://github.com/dkmstr/openuds.git
synced 2025-01-26 10:03:50 +03:00
advancing on MFA implementation
This commit is contained in:
parent
0de655d14f
commit
ec02f63cac
@ -300,27 +300,6 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
|
|||||||
"""
|
"""
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def mfaFieldName(self) -> str:
|
|
||||||
"""
|
|
||||||
This method will be invoked from the MFA form, to know the human name of the field
|
|
||||||
that will be used to enter the MFA code.
|
|
||||||
"""
|
|
||||||
return 'MFA Code'
|
|
||||||
|
|
||||||
def mfaSendCode(self) -> None:
|
|
||||||
"""
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def mfaValidate(self, identifier: str, code: str) -> None:
|
|
||||||
"""
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def providesMfa(cls) -> bool:
|
def providesMfa(cls) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -32,15 +32,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class UDSException(Exception):
|
class AuthenticatorException(Exception):
|
||||||
"""
|
|
||||||
Base exception for all UDS exceptions
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorException(UDSException):
|
|
||||||
"""
|
"""
|
||||||
Generic authentication exception
|
Generic authentication exception
|
||||||
"""
|
"""
|
||||||
@ -64,7 +56,7 @@ class InvalidAuthenticatorException(AuthenticatorException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Redirect(UDSException):
|
class Redirect(AuthenticatorException):
|
||||||
"""
|
"""
|
||||||
This exception indicates that a redirect is required.
|
This exception indicates that a redirect is required.
|
||||||
Used in authUrlCallback to indicate that redirect is needed
|
Used in authUrlCallback to indicate that redirect is needed
|
||||||
@ -73,7 +65,7 @@ class Redirect(UDSException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Logout(UDSException):
|
class Logout(AuthenticatorException):
|
||||||
"""
|
"""
|
||||||
This exceptions redirects logouts an user and redirects to an url
|
This exceptions redirects logouts an user and redirects to an url
|
||||||
"""
|
"""
|
||||||
@ -81,7 +73,7 @@ class Logout(UDSException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MFAError(UDSException):
|
class MFAError(AuthenticatorException):
|
||||||
"""
|
"""
|
||||||
This exceptions indicates than an MFA error has ocurred
|
This exceptions indicates than an MFA error has ocurred
|
||||||
"""
|
"""
|
||||||
|
@ -30,11 +30,14 @@
|
|||||||
"""
|
"""
|
||||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||||
"""
|
"""
|
||||||
|
import datetime
|
||||||
|
import random
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from django.utils.translation import ugettext_noop as _
|
from django.utils.translation import ugettext_noop as _
|
||||||
from uds.core.services import types as serviceTypes
|
from uds.models import getSqlDatetime
|
||||||
from uds.core import Module
|
from uds.core import Module
|
||||||
|
from uds.core.auths import exceptions
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from uds.core.environment import Environment
|
from uds.core.environment import Environment
|
||||||
@ -71,6 +74,14 @@ class MFA(Module):
|
|||||||
# : your own :py:meth:uds.core.module.BaseModule.icon method.
|
# : your own :py:meth:uds.core.module.BaseModule.icon method.
|
||||||
iconFile: typing.ClassVar[str] = 'mfa.png'
|
iconFile: typing.ClassVar[str] = 'mfa.png'
|
||||||
|
|
||||||
|
# : Cache time for the generated MFA code
|
||||||
|
# : this means that the code will be valid for this time, and will not
|
||||||
|
# : be resent to the user until the time expires.
|
||||||
|
# : This value is in seconds
|
||||||
|
# : Note: This value is used by default "process" methos, but you can
|
||||||
|
# : override it in your own implementation.
|
||||||
|
cacheTime: typing.ClassVar[int] = 300
|
||||||
|
|
||||||
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)
|
||||||
@ -90,3 +101,68 @@ class MFA(Module):
|
|||||||
|
|
||||||
Default implementation does nothing
|
Default implementation does nothing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def label(self) -> str:
|
||||||
|
"""
|
||||||
|
This method will be invoked from the MFA form, to know the human name of the field
|
||||||
|
that will be used to enter the MFA code.
|
||||||
|
"""
|
||||||
|
return 'MFA Code'
|
||||||
|
|
||||||
|
def validity(self) -> int:
|
||||||
|
"""
|
||||||
|
This method will be invoked from the MFA form, to know the validity in secods
|
||||||
|
of the MFA code.
|
||||||
|
If value is 0 or less, means the code is always valid.
|
||||||
|
"""
|
||||||
|
return self.cacheTime
|
||||||
|
|
||||||
|
def sendCode(self, code: str) -> None:
|
||||||
|
"""
|
||||||
|
This method will be invoked from "process" method, to send the MFA code to the user.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError('sendCode method not implemented')
|
||||||
|
|
||||||
|
def process(self, userId: str, identifier: str) -> None:
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
Default implementation generates a random code and sends invokes "sendCode" method.
|
||||||
|
"""
|
||||||
|
# try to get the stored code
|
||||||
|
data: typing.Any = self.storage.getPickle(userId)
|
||||||
|
try:
|
||||||
|
if data:
|
||||||
|
# if we have a stored code, check if it's still valid
|
||||||
|
if data[0] + datetime.timedelta(seconds=self.cacheTime) < getSqlDatetime():
|
||||||
|
# if it's still valid, just return without sending a new one
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
# if we have a problem, just remove the stored code
|
||||||
|
self.storage.remove(userId)
|
||||||
|
|
||||||
|
# Generate a 6 digit code (0-9)
|
||||||
|
code = ''.join(random.SystemRandom().choices('0123456789', k=6))
|
||||||
|
# Store the code in the database, own storage space
|
||||||
|
self.storage.putPickle(userId, (getSqlDatetime(), code))
|
||||||
|
# Send the code to the user
|
||||||
|
self.sendCode(code)
|
||||||
|
|
||||||
|
def validate(self, userId: str, identifier: str, code: str) -> None:
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
# Validate the code
|
||||||
|
try:
|
||||||
|
data = self.storage.getPickle(userId)
|
||||||
|
if data and len(data) == 2:
|
||||||
|
# Check if the code is valid
|
||||||
|
if data[1] == code:
|
||||||
|
# Code is valid, remove it from storage
|
||||||
|
self.storage.remove(userId)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
# Any error means invalid code
|
||||||
|
raise exceptions.MFAError(e)
|
||||||
|
|
||||||
|
0
server/src/uds/templates/uds/modern/mfa.html
Normal file
0
server/src/uds/templates/uds/modern/mfa.html
Normal file
@ -128,7 +128,7 @@ urlpatterns = [
|
|||||||
name='page.ticket.launcher',
|
name='page.ticket.launcher',
|
||||||
),
|
),
|
||||||
# MFA authentication
|
# MFA authentication
|
||||||
path('uds/page/mfa/', uds.web.views.modern.mfa, name='page.mfa'),
|
path(r'uds/page/mfa/', uds.web.views.modern.mfa, name='page.mfa'),
|
||||||
# This must be the last, so any patition will be managed by client in fact
|
# This must be the last, so any patition will be managed by client in fact
|
||||||
re_path(r'uds/page/.*', uds.web.views.modern.index, name='page.placeholder'),
|
re_path(r'uds/page/.*', uds.web.views.modern.index, name='page.placeholder'),
|
||||||
# Utility
|
# Utility
|
||||||
|
@ -144,6 +144,7 @@ def udsJs(request: 'ExtendedHttpRequest') -> str:
|
|||||||
'authenticators': [
|
'authenticators': [
|
||||||
getAuthInfo(auth) for auth in authenticators if auth.getType()
|
getAuthInfo(auth) for auth in authenticators if auth.getType()
|
||||||
],
|
],
|
||||||
|
'mfa': request.session.get('mfa', None),
|
||||||
'tag': tag,
|
'tag': tag,
|
||||||
'os': request.os['OS'].value[0],
|
'os': request.os['OS'].value[0],
|
||||||
'image_size': Image.MAX_IMAGE_SIZE,
|
'image_size': Image.MAX_IMAGE_SIZE,
|
||||||
@ -164,6 +165,7 @@ def udsJs(request: 'ExtendedHttpRequest') -> str:
|
|||||||
'urls': {
|
'urls': {
|
||||||
'changeLang': reverse('set_language'),
|
'changeLang': reverse('set_language'),
|
||||||
'login': reverse('page.login'),
|
'login': reverse('page.login'),
|
||||||
|
'mfa': reverse('page.mfa'),
|
||||||
'logout': reverse('page.logout'),
|
'logout': reverse('page.logout'),
|
||||||
'user': reverse('page.index'),
|
'user': reverse('page.index'),
|
||||||
'customAuth': reverse('uds.web.views.customAuth', kwargs={'idAuth': ''}),
|
'customAuth': reverse('uds.web.views.customAuth', kwargs={'idAuth': ''}),
|
||||||
|
@ -157,15 +157,17 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
|
|||||||
|
|
||||||
# Obtain MFA data
|
# Obtain MFA data
|
||||||
authInstance = request.user.manager.getInstance()
|
authInstance = request.user.manager.getInstance()
|
||||||
|
mfaInstance = mfaProvider.getInstance()
|
||||||
|
|
||||||
mfaIdentifier = authInstance.mfaIdentifier()
|
mfaIdentifier = authInstance.mfaIdentifier()
|
||||||
mfaFieldName = authInstance.mfaFieldName()
|
label = mfaInstance.label()
|
||||||
|
|
||||||
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():
|
||||||
code = form.cleaned_data['code']
|
code = form.cleaned_data['code']
|
||||||
try:
|
try:
|
||||||
authInstance.mfaValidate(mfaIdentifier, code)
|
mfaInstance.validate(str(request.user.pk), mfaIdentifier, code)
|
||||||
request.authorized = True
|
request.authorized = True
|
||||||
return HttpResponseRedirect(reverse('page.index'))
|
return HttpResponseRedirect(reverse('page.index'))
|
||||||
except exceptions.MFAError as e:
|
except exceptions.MFAError as e:
|
||||||
@ -174,12 +176,12 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
|
|||||||
else:
|
else:
|
||||||
pass # Will render again the page
|
pass # Will render again the page
|
||||||
else:
|
else:
|
||||||
# First, make MFA send a code
|
# Make MFA send a code
|
||||||
authInstance.mfaSendCode()
|
mfaInstance.process(str(request.user.pk), mfaIdentifier)
|
||||||
|
|
||||||
# Redirect to index, but with MFA data
|
# Redirect to index, but with MFA data
|
||||||
request.session['mfa'] = {
|
request.session['mfa'] = {
|
||||||
'identifier': mfaIdentifier,
|
'label': label,
|
||||||
'fieldName': mfaFieldName,
|
'validity': mfaInstance.validity(),
|
||||||
}
|
}
|
||||||
return HttpResponseRedirect(reverse('page.index'))
|
return HttpResponseRedirect(reverse('page.index'))
|
Loading…
x
Reference in New Issue
Block a user