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 django-developers+unsubscr...@googlegroups.com. To post to this group, send email to django-developers@googlegroups.com. 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.