User authentication

In this section, we ll look into built-in tools that are used for user authentication

Passwords

Make sure these two apps are listed in settings.py (They are usualy preloaded. If you load them manually, remember to migrate)

INSTALLED_APPS = [
            'django.contrib.auth',
            'django.contrib.contenttypes',
        ]

Never store passwords as plain text! Use django's built-in hashing algorithm (SHA - Secure Hashing algorithm).

Django's default hashing algorithm is PBKDF2

For more secure hashing algorithms, we can use state-of-art applications like bcrypt and argon2

pip install bcrypt
pip install django[argon2]

Inside of settings.py you can then pass in the list of PASSWORD_HASHERS to try in the order you want to try them.

PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.BCryptPasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
]

Django's built-in password validators

##Django's built in password validators
AUTH_PASSWORD_VALIDATORS = [
    {   #Checks if password is similar to user name or other attributes
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {   #Checks for min length
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        #We can pass options to modify behaviour
        'OPTION': {'min_length':9}, 
    },
    {   #checks for weak password
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {   #Make sure password has numbers
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

Check documentation for list of password validation and its options

Create media directory

The media contents uploaded by the user(eg, profile pic) are stored under media directory. Make sure to add this directory in settings.py

# MEDIA INFORMATION:
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

To view these media through the admin interface, the following lines should be included in urls.py

from django.conf import settings

    if settings.DEBUG:
        from django.conf.urls.static import static
        from django.contrib.staticfiles.urls import staticfiles_urlpatterns

        # Serve static and media files from development server
        urlpatterns += staticfiles_urlpatterns()
        urlpatterns += static(settings.MEDIA_URL,
                            document_root=settings.MEDIA_ROOT)

Storing User data in admin interface

There is a default User and Groups field in the admin interface which stores the information of the superusers etc.

To create a form to fill this field, use the User object in forms.py

#Import User from authorization models
    from django.contrib.auth.models import User
    #Attributes under User :

    # username
    # password
    # email
    # first_name
    # last_name

    class UserForm(forms.ModelForm):

        password = forms.CharField(widget=forms.PasswordInput())

        class Meta:
            model = User
            fields = ('username','email','password')

Most of the times, we may want to extend the attributes of the User object. We do this in models.py by creating a class that has-the fields of User object

from django.db import models
    #Import User from authorization models
    from django.contrib.auth.models import User
    # Create your models here.

    class UserProfileInfo(models.Model):

        #Has-a relationship with User class!
        user = models.OneToOneField(User,on_delete=models.CASCADE)

        #additional attributes
        portfolio_site = models.URLField(blank=True)
        #upload_to='profile_pics' requires profile_pics to be a sub-dir under media
        # pip install pillow to use this!
        profile_pic = models.ImageField(upload_to='profile_pics',blank=True)

        def __str__(self):
            return self.user.username

In forms.py we add

from app5.models import UserProfileInfo

    class UserProfileInfoForm(forms.ModelForm):

        class Meta:
            model = UserProfileInfo
            fields = ('portfolio_site','profile_pic')

In views.py, there are several important things to note:

1) Passwords should be hashed and saved

2) In case of NULL in the fields with required=True, code will break when trying to save to DB. Therefore, use commit=False

3) Media files should be manually set

from django.shortcuts import render
    from app5 import forms

    # Create your views here.
    def index(request):
        return render(request,'index.html')

    def register(request):

            registered=False

            if request.method == "POST":
                user_form = forms.UserForm(request.POST)
                profile_form = forms.UserProfileInfoForm(request.POST)

                if user_form.is_valid() and profile_form.is_valid():

                    user = user_form.save()
                    #Hash the password. Without this, hashing algorithm is not applied and password is not saved
                    user.set_password(user.password)
                    user.save()

                    #commit = false does NOT save into database
                    #Without this, the code breaks because profile.user is NULL
                    profile = profile_form.save(commit=False)
                    #one-to-one relation with user. We dont apply this in the form field
                    profile.user = user

                    #Without this, the image is not saved in the directory nor shown in the admin
                    if 'profile_pic' in request.FILES:
                        profile.profile_pic = request.FILES['profile_pic']

                    profile.save()

                    registered = True
                else:
                    print(user_form.errors,profile_form.errors)

            else:
                    user_form = forms.UserForm()
                    profile_form = forms.UserProfileInfoForm()

            return render(request,'registration.html',
                                 {'registered':registered,
                                  'user_form':user_form,
                                  'profile_form':profile_form})

Login user

In settings.py add redirect url

#LOG IN URL (shoule match with urls.py)
LOGIN_URL = '/app5/user_login/'

Provide the validation in views.py

from django.contrib.auth import authenticate, login, logout
    from django.http import HttpResponse, HttpResponseRedirect
    #reverse(name) does the samething as {%  url 'name' %}
    from django.urls import reverse
    # Upon decorating, this view requires the user to be logged in to render
    from django.contrib.auth.decorators import login_required

    def user_login(request): #renders login.html or redirects to homepage on login

        if request.method == 'POST':
            #Get the fields
            username = request.POST.get('username') #login.html has a form with field names 'username'
            password = request.POST.get('password')

            #Authenticates the password for the user
            #This return variable can be accessed across all HTML templates!
            user = authenticate(username=username,password=password)

            if user:
                if user.is_active:
                    #The user remains logged in across the whole project
                    login(request,user)
                    #redirect to index.html
                    return HttpResponseRedirect(reverse('index'))
                else:
                    return HttpResponse("Account not active")
            else:
                print(username, " tried to login with password ", password)
                return HttpResponse("Invalid login details supplied!")
        else:
            return render(request,'login.html')


    #This code breaks if there is no login
    @login_required
    def user_logout(request):
        #The user is not logged out until this link is clicked
        logout(request)
        return HttpResponseRedirect(reverse('index'))

In urls.py

path("logout/",views.user_logout,name='logout'),
    path("user_login/",views.user_login,name='user_login'),

Using the authentication variable in HTML

<div class="jumbotron">
  <!-- Tag user is available from views.py
    user = authenticate(username=username,password=password) -->
  {%  if user.is_active  %}
    <h1> Hello </h1>
  {%  else  %}
    <h1>Hello user!</h1>
  {%  endif  %}
</div>

Check documentation for all attributes