mirror of
https://github.com/dkmstr/openuds.git
synced 2024-12-22 13:34:04 +03:00
Added flow to MFA description, to easy development of it
Also, added IP for "remembe device" information, so only when device stays on same ip, will not be asked for a new MFA
This commit is contained in:
parent
2d2c20909d
commit
7cb494c4d5
@ -304,10 +304,12 @@ class Authenticator(Module):
|
||||
If this method is provided by an authenticator, the user will be allowed to enter a MFA code
|
||||
You must return the value used by a MFA provider to identify the user (i.e. email, phone number, etc)
|
||||
If not provided, or the return value is '', the user will be allowed to access UDS without MFA
|
||||
only if the mfa itself allows empty mfaIdentifier. (look at mfa base, allow_login_without_identifier)
|
||||
|
||||
Note: Field capture will be responsible of provider. Put it on MFA tab of user form.
|
||||
Take into consideration that mfaIdentifier will never be invoked if the user has not been
|
||||
previously authenticated. (that is, authenticate method has already been called)
|
||||
So, you can store the mfaIdentifier at authenticate method, and return it here for example.
|
||||
"""
|
||||
return ''
|
||||
|
||||
|
@ -54,6 +54,34 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# MFA flow:
|
||||
# 1.- User logs in
|
||||
# 2.- If user has no MFA, login is allowed
|
||||
# 3.- If remember_device (stored on DB) is active, and the remember_device cookie is valid, login is allowed
|
||||
# 4.- If user has MFA, and remember_device is not active, or the cookie is not valid, MFA is requested
|
||||
# 5.- The MFA identifier is requested to the authenticator (phone, email, etc).
|
||||
# - If the identifier is empty, "allow_login_without_identifier" method form MFA is called and processed acordly
|
||||
# - If the method returns True, login is allowed
|
||||
# - If the method returns False, login is denied
|
||||
# - If the method returns None, the MFA is processed with an empty identifier
|
||||
# 6.- The process method is called, and the MFA code is sent to the user (or whatever the MFA method does)
|
||||
# - If returns MFA.RESULT.OK, the MFA code was sent, the MFA form is shown to the user with:
|
||||
# - The label of the field to enter the MFA code (label method)
|
||||
# - The HTML to be shown below the MFA code form (html method)
|
||||
# - If returns MFA.RESULT.ALLOWED, the MFA code was not sent, the user does not need to enter the MFA code, login is done
|
||||
# - If raises an error, the MFA code was not sent, and the user is shown an error
|
||||
# 7.- The user enters the MFA code, and POSTs the form
|
||||
# 8.- The validate method is called with the MFA code and rest of parameters
|
||||
# - If the code is valid, the user is allowed to login (returns)
|
||||
# - If the code is not valid, an exception of type 'exceptions.auth.MFAError' is raised
|
||||
# - The exception message will be shown to the user
|
||||
# - The MFA code will be retried config.GlobalConfig.MAX_LOGIN_TRIES times at most
|
||||
# 9.- If the user is allowed to login, the remember_device cookie is set if the user has checked the remember_device checkbox
|
||||
# (this part is in fact done by the login view, but it's part of the MFA flow, just to remember it here)
|
||||
|
||||
|
||||
# Note: if the MFA process takes too long, the user will be redirected to the login page, and the process will start again
|
||||
|
||||
|
||||
class LoginAllowed(enum.StrEnum):
|
||||
"""
|
||||
@ -363,7 +391,7 @@ class MFA(Module):
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_user_id(user: 'User') -> str:
|
||||
def get_user_unique_id(user: 'User') -> str:
|
||||
"""
|
||||
Composes an unique, mfa dependant, id for the user (at this time, it's sha3_256 of user + mfa)
|
||||
"""
|
||||
|
@ -188,7 +188,7 @@ class User(UUIDModel, properties.PropertiesMixin):
|
||||
"""
|
||||
# If has mfa, remove related data
|
||||
if self.manager.mfa:
|
||||
self.manager.mfa.get_instance().reset_data(mfas.MFA.get_user_id(self))
|
||||
self.manager.mfa.get_instance().reset_data(mfas.MFA.get_user_unique_id(self))
|
||||
|
||||
@staticmethod
|
||||
def pre_delete(sender: typing.Any, **kwargs: typing.Any) -> None: # pylint: disable=unused-argument
|
||||
|
@ -173,23 +173,26 @@ def mfa(
|
||||
|
||||
store: 'storage.Storage' = storage.Storage('mfs')
|
||||
|
||||
mfa_provider = request.user.manager.mfa # typing.cast('None|models.MFA',
|
||||
mfa_provider = request.user.manager.mfa # Get MFA provider for user
|
||||
if not mfa_provider:
|
||||
logger.warning('MFA: No MFA provider for user')
|
||||
return HttpResponseRedirect(reverse('page.index'))
|
||||
|
||||
mfa_user_id = mfas.MFA.get_user_id(request.user)
|
||||
mfa_user_id = mfas.MFA.get_user_unique_id(request.user)
|
||||
|
||||
# Try to get cookie anc check it
|
||||
mfa_cookie = request.COOKIES.get(consts.auth.MFA_COOKIE_NAME, None)
|
||||
if mfa_cookie and mfa_provider.remember_device > 0:
|
||||
stored_user_id: typing.Optional[str]
|
||||
created: typing.Optional[datetime.datetime]
|
||||
stored_user_id, created = store.read_pickled(mfa_cookie) or (None, None)
|
||||
stored_data = store.read_pickled(mfa_cookie) or (None, None, None)
|
||||
stored_user_id, created, ip = (stored_data + (None,))[:3]
|
||||
if (
|
||||
stored_user_id
|
||||
and created
|
||||
and created + datetime.timedelta(hours=mfa_provider.remember_device) > datetime.datetime.now()
|
||||
# Old stored values do not have ip, so we need to check it
|
||||
and (not ip or ip == request.ip)
|
||||
):
|
||||
# Cookie is valid, skip MFA setting authorization
|
||||
logger.debug('MFA: Cookie is valid, skipping MFA')
|
||||
@ -261,7 +264,7 @@ def mfa(
|
||||
mfa_cookie = CryptoManager().random_string(96)
|
||||
store.save_pickled(
|
||||
mfa_cookie,
|
||||
(mfa_user_id, now),
|
||||
(mfa_user_id, now, request.ip), # MFA will only be valid for this user and this ip
|
||||
)
|
||||
response.set_cookie(
|
||||
consts.auth.MFA_COOKIE_NAME,
|
||||
@ -280,6 +283,9 @@ def mfa(
|
||||
# Too many tries, redirect to login error page
|
||||
return errors.error_view(request, types.errors.Error.ACCESS_DENIED)
|
||||
return errors.error_view(request, types.errors.Error.INVALID_MFA_CODE)
|
||||
except Exception as e:
|
||||
logger.error('Error processing MFA: %s', e)
|
||||
return errors.error_view(request, types.errors.Error.UNKNOWN_ERROR)
|
||||
else:
|
||||
pass # Will render again the page
|
||||
else:
|
||||
|
Loading…
Reference in New Issue
Block a user