I would like to explain a potential solution I have been working on (See 
commit https://github.com/mlevental/django/tree/ticket_25612). 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/3a0dd3b5-ac1e-4ff4-bbcc-7da964e5acad%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to