I would like to explain a potential solution I have been working on (See
commit
https://github.com/mlevental/django/commit/51dbaa6748076e06d91b361c2fa60ecf24f5c27e
<https://www.google.com/url?q=https%3A%2F%2Fgithub.com%2Fmlevental%2Fdjango%2Ftree%2Fticket_25612&sa=D&sntz=1&usg=AFQjCNG1KKSu5POu77xHfm-LQ1z2F--Rew>).
I think it's not complete but I don't have the time to continue working on
it.
*Overview:*
- In order to check if a user is authenticated with one or two factors,
the attributes *is_one_factor_authenticated* and
*is_two_factor_authenticated* were added to *AbstractBaseUser *(and
therefore to *AbstractUser *and *User*) and *AnonymousUser*.
- For *AnonymousUser *both attributes are always False. For
*AbstractBaseUser
is_one_factor_authenticated* is always True because an authenticated
user is authenticated with at least one factor, *is_two_factor_authenticated
*is explained in the next paragraph.
- *is_authenticated* stays unchanged but the meaning becomes
"authenticated with one or two factors". This way an application can have
users who use 1FA and others who enabled 2FA.
- Similar to the backends in django.contrib.auth there are backends for
2FA. But instead of returning a *User *object they must return a *Device
*object on success.
- *Device *is a model class that encapsulates all the necessary data of
a 2FA method for a single user, e.g. cryptographic keys. It can but doesn't
have to represent a real device.
- After a successful two factor authentication the device is stored in
the session. The middleware *TFAMiddleware *(which must be installed
after *AuthenticationMiddleware*) sets this device for the user coming
from the request object.
- *is_two_factor_authenticated* of *AbstractBaseUser *evaluates to True
only if a device is set.
- The authentication process is handled by two views. *FirstFactorLoginView
*is responsible for authenticating with the first factor and redirecting
to *SecondFactorLoginView *on success. *SecondFactorLoginView *handles
the authentication with the second factor and redirects to a defined URL on
success. If the user accessing the *SecondFactorLoginView* isn't
authenticated with the first factor he is redirected to the
*FirstFactorLoginView*.
- The *SecondFactorLoginView *can have multiple forms.
- This is done because different 2FA methods can require different input
fields (e.g different input types, labels, number of input fields).
- Also this allows to display multiple forms, for example when it is
desired to show the form for the default 2FA method and the backup method
on one page.
- By default *SecondFactorLoginView *loads all the forms specified in
the setting *TFA_FORMS*. Which forms are displayed can be adjusted in
the template or by overriding *get_form_classes()*. Only one form
gets validated on a POST request. Therefore when submitting the form the
2FA method name needs to be included as a HTTP POST parameter.
- For example if the setting *TFA_FORMS* is the following:
TFA_FORMS = [
{'METHOD_NAME': 'TOTP', 'FORM_PATH':
'django.contrib.twofactorauth.forms.TOTPAuthenticationForm'},
{'METHOD_NAME': 'Backup Token', 'FORM_PATH':
'django.contrib.twofactorauth.forms.BackupTokenAuthenticationForm'},
]
and the *TOTPAuthenticationForm *is submitted, then
type=TOTP must be included, for example in a button tag:
<form method="POST">
{% csrf_token %}
{{ forms.TOTP.as_p }}
<button name="type" value="{{ forms.TOTP.method_name }}">{% trans
'Submit' %}</button>
</form>
- For convenience there is the *tfa_required* decorator and the mixin
*TFARequiredMixin*. They work analogous to the *login_required*
decorator and *LoginRequiredMixin *from django.contrib.auth. Instead of
checking for *is_authenticated* they check for
*is_two_factor_authenticated*. If the user is not two factor
authenticated he is redirected to a specified URL.
- For example the URL can be specified by setting *TFA_LOGIN_URL* to
"tfa:first_factor_login" and would point to the *FirstFactorLoginView*.
By default the *FirstFactorLoginView *requires the user to authenticate
with the first factor and redirects on success to *SecondFactorLoginView*.
If desired the *FirstFactorLoginView *can redirct an already one factor
authenticated user to *SecondFactorLoginView *right away, for this
*redirect_authenticated_user* needs to be set to True. After
successfully authenticating with the second factor the user is redirected
to the initially inaccessible page. For this last redirect to work the
redirect URL is transfered from *FirstFactorLoginView *to
*SecondFactorLoginView
*by appending it as a HTTP GET parameter.
- For the admin site to support 2FA the class *AdminSiteTFARequired *was
created which inherits from *AdminSite *and the new mixin
*AdminSiteTFARequiredMixin*. This mixin overrides *has_permission()* so
that admin urls can only be accessed by two factor authenticated users.
*login()* is overriden to call the correct view depending on the
authentication status of the user: requested admin url or
*FirstFactorLoginView* or *SecondFactorLoginView*.
*Incomplete or missing features:*
- With this solution access to views can be granted only to users who
are two factor authenticated. But there is no view, decorator or mixin for
the case when a view should be accessible to both users who wish to use 1FA
and users who use 2FA.
- The current login required decorator and mixin can't be used for
this because they only check for *is_authenticated *and thus they
will also let in users who have setup 2 factors but are authenticated
with
only 1.
- The naive way to do this would probably be to check whether the
non-authenticated user needs to authenticate with 1 or 2 factors and
redirect to the according login view (*LoginView *or
*FirstFactorLoginView*). But the check can't be performed for
non-authenticated users because they are instances of the class
*AnonymousUser*. So the check needs to be performed when the user is
authenticated with at least the first factor.
- some helper methods/views were implemented:
- *has_tfa_enabled(use*r*)* returns True if the user has at least
one device. This will return False for *AnonymousUser*.
- *is_tfa_required(user) *returns True if either 2FA is forced for
every user or 2FA is optional and the user has enabled 2FA. This will
return False for *AnonymousUser *if 2FA is optional.
- *TFADisableView *is a view for disabling 2FA. Disabling means
deleting all devices for the user. This way *has_tfa_enabled(user)*
will return False.
- TOTP can be used as a 2FA method but the setup/registration is not
implemented. For TOTP to work the client needs to receive some
configuration data. This data is usually displayed as a QR code and is
scanned with an app like Google Authenticator.
- In the class TOTPDevice the configuration data is accessible via
the property config_url. Generating the QR code from config_url works for
example with the library qrcode.
--
You received this message because you are subscribed to the Google Groups
"Django developers (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/django-developers.
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-developers/57c1dbbe-47ac-4222-8140-67b118636a07%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.