mirror of
https://github.com/dkmstr/openuds.git
synced 2025-01-12 09:17:56 +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 ''
|
||||
|
||||
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
|
||||
def providesMfa(cls) -> bool:
|
||||
"""
|
||||
|
@ -32,15 +32,7 @@
|
||||
"""
|
||||
|
||||
|
||||
class UDSException(Exception):
|
||||
"""
|
||||
Base exception for all UDS exceptions
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AuthenticatorException(UDSException):
|
||||
class AuthenticatorException(Exception):
|
||||
"""
|
||||
Generic authentication exception
|
||||
"""
|
||||
@ -64,7 +56,7 @@ class InvalidAuthenticatorException(AuthenticatorException):
|
||||
pass
|
||||
|
||||
|
||||
class Redirect(UDSException):
|
||||
class Redirect(AuthenticatorException):
|
||||
"""
|
||||
This exception indicates that a redirect is required.
|
||||
Used in authUrlCallback to indicate that redirect is needed
|
||||
@ -73,7 +65,7 @@ class Redirect(UDSException):
|
||||
pass
|
||||
|
||||
|
||||
class Logout(UDSException):
|
||||
class Logout(AuthenticatorException):
|
||||
"""
|
||||
This exceptions redirects logouts an user and redirects to an url
|
||||
"""
|
||||
@ -81,7 +73,7 @@ class Logout(UDSException):
|
||||
pass
|
||||
|
||||
|
||||
class MFAError(UDSException):
|
||||
class MFAError(AuthenticatorException):
|
||||
"""
|
||||
This exceptions indicates than an MFA error has ocurred
|
||||
"""
|
||||
|
@ -30,11 +30,14 @@
|
||||
"""
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import datetime
|
||||
import random
|
||||
import typing
|
||||
|
||||
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.auths import exceptions
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds.core.environment import Environment
|
||||
@ -71,6 +74,14 @@ class MFA(Module):
|
||||
# : your own :py:meth:uds.core.module.BaseModule.icon method.
|
||||
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):
|
||||
super().__init__(environment, values)
|
||||
self.initialize(values)
|
||||
@ -90,3 +101,68 @@ class MFA(Module):
|
||||
|
||||
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',
|
||||
),
|
||||
# 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
|
||||
re_path(r'uds/page/.*', uds.web.views.modern.index, name='page.placeholder'),
|
||||
# Utility
|
||||
|
@ -144,6 +144,7 @@ def udsJs(request: 'ExtendedHttpRequest') -> str:
|
||||
'authenticators': [
|
||||
getAuthInfo(auth) for auth in authenticators if auth.getType()
|
||||
],
|
||||
'mfa': request.session.get('mfa', None),
|
||||
'tag': tag,
|
||||
'os': request.os['OS'].value[0],
|
||||
'image_size': Image.MAX_IMAGE_SIZE,
|
||||
@ -164,6 +165,7 @@ def udsJs(request: 'ExtendedHttpRequest') -> str:
|
||||
'urls': {
|
||||
'changeLang': reverse('set_language'),
|
||||
'login': reverse('page.login'),
|
||||
'mfa': reverse('page.mfa'),
|
||||
'logout': reverse('page.logout'),
|
||||
'user': reverse('page.index'),
|
||||
'customAuth': reverse('uds.web.views.customAuth', kwargs={'idAuth': ''}),
|
||||
|
@ -157,15 +157,17 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
|
||||
|
||||
# Obtain MFA data
|
||||
authInstance = request.user.manager.getInstance()
|
||||
mfaInstance = mfaProvider.getInstance()
|
||||
|
||||
mfaIdentifier = authInstance.mfaIdentifier()
|
||||
mfaFieldName = authInstance.mfaFieldName()
|
||||
label = mfaInstance.label()
|
||||
|
||||
if request.method == 'POST': # User has provided MFA code
|
||||
form = MFAForm(request.POST)
|
||||
if form.is_valid():
|
||||
code = form.cleaned_data['code']
|
||||
try:
|
||||
authInstance.mfaValidate(mfaIdentifier, code)
|
||||
mfaInstance.validate(str(request.user.pk), mfaIdentifier, code)
|
||||
request.authorized = True
|
||||
return HttpResponseRedirect(reverse('page.index'))
|
||||
except exceptions.MFAError as e:
|
||||
@ -174,12 +176,12 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
|
||||
else:
|
||||
pass # Will render again the page
|
||||
else:
|
||||
# First, make MFA send a code
|
||||
authInstance.mfaSendCode()
|
||||
# Make MFA send a code
|
||||
mfaInstance.process(str(request.user.pk), mfaIdentifier)
|
||||
|
||||
# Redirect to index, but with MFA data
|
||||
request.session['mfa'] = {
|
||||
'identifier': mfaIdentifier,
|
||||
'fieldName': mfaFieldName,
|
||||
'label': label,
|
||||
'validity': mfaInstance.validity(),
|
||||
}
|
||||
return HttpResponseRedirect(reverse('page.index'))
|
Loading…
Reference in New Issue
Block a user