Django Logging Users In and Out

Django provides built-in URL patterns and view functions which makes adding login and logout system to your site a breeze. But before we add them to our project, we will create login and logout system on our own by using some utility functions provided by the Django authentication framework.

The authenticate() and login() functions #

Django authentication framework (django.contrib.auth) provides authenticate() and login() functions whose job is to authenticate and login users respectively.

The authenticate() function accepts two keyword arguments, username and password and returns an object of type User, if username and password are valid. Otherwise, it returns None.

>>>
>>> from django.contrib import auth
>>>
>>> user = auth.authenticate(username='noisyboy', password='pass')
>>> user
<User: noisyboy>
>>>
>>> if user is not None:
...   print("Credentials are valid")
... else:
...   print("Invalid Credentials")
...
Credentials are valid
>>>
>>>

The authenticate() function only verifies whether the credentials provided are valid or not. It doesn't login the user.

To login user we use login() function. It takes two arguments, request object (HttpRequest) and a User object. To login user it saves the user's ID in the session, using Django session framework.

Once a user is logged in, he should be able to logout and this is the responsibility of logout() function.

The logout() function #

To logout users we use logout() function. It accepts a request (HttpRequest) object and returns None. Calling logout() function completely deletes the session data associated with the logged in user.

It is important to note that calling logout() function doesn't throw any errors if the user is not logged in.

The logout() function also removes the cookie from the browser.

Another Login System #

Now we have enough knowledge to roll out our own login system.

In the blog's app views.py add login(), logout() and admin_page() views as follows:

TGDB/django_project/blog/views.py

#...
from django_project import helpers
from django.contrib import auth
#...

def lousy_logout(request):
    #...

def login(request):
    if request.user.is_authenticated():
        return redirect('admin_page')

    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        user = auth.authenticate(username=username, password=password)

        if user is not None:
            # correct username and password login the user
            auth.login(request, user)
            return redirect('admin_page')

        else:
            messages.error(request, 'Error wrong username/password')

    return render(request, 'blog/login.html')


def logout(request):
    auth.logout(request)
    return render(request,'blog/logout.html')


def admin_page(request):
    if not request.user.is_authenticated():
        return redirect('blog_login')

    return render(request, 'blog/admin_page.html')

Then create three templates login.html, logout.html and admin_page.html as follows:

TGDB/django_project/blog/templates/blog/login.html

{% extends "blog/base.html"  %}

{% block title %}
    Blog - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="content">
        <div class="section-inner clearfix">

        <h3>Login Form</h3>

        {% if messages %}
            <ul>
            {% for message in messages %}
                <li>{{ message }}</li>
            {% endfor %}
            </ul>
        {% endif %}

        <form action="" method="post">
            {% csrf_token %}
            <table>
                <tr>
                    <td><label for="id_username">Enter username</label></td>
                    <td><input type="text" id="id_username" name="username"></td>
                </tr>
                <tr>
                    <td><label for="id_username">Enter password</label></td>
                    <td><input type="password" id="id_password" name="password"></td>
                </tr>
                <tr>
                    <td></td>
                    <td><input type="submit" value="Submit"></td>
                </tr>
            </table>
        </form>

        </div>
    </div>

{% endblock %}

TGDB/django_project/blog/templates/blog/logout.html

{% extends "blog/base.html"  %}

{% block title %}
    Blog - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="content">
        <div class="section-inner clearfix">

        <p>You are logged out. <a href="{% url 'blog_login' %}">Click here</a> to login again.</p>

        </div>
    </div>

{% endblock %}

TGDB/django_project/blog/templates/blog/admin_page.html

{% extends "blog/base.html"  %}

{% block title %}
    Blog - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="content">
        <div class="section-inner clearfix">

        <p>Welcome {{ request.user.username }} !</p>

        <p>User Details:</p>

        <ul>
            <li>Email: {{ request.user.username.email|default:"NA" }}</li>
            <li>SuperUser: {{ request.user.is_superuser }}</li>
            <li>Staff: {{ request.user.is_staff }}</li>
            <li>Date Joined: {{ request.user.date_joined }}</li>
            <li>Last Login: {{ request.user.last_login }}</li>
        </ul>

        <p><a href="{% url 'blog_logout' %}">Logout</a></p>

        </div>
    </div>

{% endblock %}

Finally, add the following URL patterns in the blog's urls.py file:

TGDB/django_project/blog/urls.py

#...
urlpatterns = [
    url(r'^login/$', views.login, name='blog_login'),
    url(r'^logout/$', views.logout, name='blog_logout'),
    url(r'^admin_page/$', views.admin_page, name='admin_page'),
    url(r'^lousy-login/$', views.lousy_login, name='lousy_login'),
    #...
]

Start the development server and visit http://127.0.0.1:8000/login/. You should get a page like this:

login-page.png

Enter correct username and password and you will be greeted with a page like this:

admin-page.png

Nothing extraordinary here, we are just using some of the attributes we have learned in earlier lesson to get some information about the current logged in user.

To logout, click the logout link at the bottom of the page.

logout-page.png

Using built-in login() and logout() views #

Django provides two views django.contrib.auth.login() and django.contrib.auth.logout() to login and logout users respectively.

To use these views, import django.contrib.auth package and then add the following two URL patterns in the cadmin's app urls.py file:

TGDB/django_project/cadmin/urls.py

#...
from . import views
from django.contrib.auth import views as auth_views

urlpatterns = [
    url(r'^accounts/login/$', auth_views.login, name='login'),
    url(r'^accounts/logout/$', auth_views.logout, name='logout'),
    url(r'^post/add/$', views.post_add, name='post_add'),
    #...
]

By default, Django uses /accounts/login/ and /accounts/logout/ URL for login and logout respectively.

Save the urls.py file and visit http://127.0.0.1:8000/cadmin/accounts/login/. You will get a TemplateDoesNotExist exception as follows:

templatedoesnotexist-image.png

The problem is that by default, django.contrib.auth.login() view looks for registration/login.html template in the templates directory of the auth app (django.contrib.auth). Because there is no such file in the templates directory of the auth app, Django raises a TemplateDoesNotExist exception.

We can pass a different template to the django.contrib.auth.login() view using the template_name keyword argument as follows:

url(r'^accounts/login/$',
    auth_views.login, 
    {'template_name': 'blog/login.html'}, 
    name='login'
)

Similarly, by default the django.contrib.auth.logout() view uses registration/logged_out.html template from the admin app (django.contrib.admin). This is the same template which you would see if you logout from the Django Admin site.

Visit http://127.0.0.1:8000/cadmin/accounts/logout/ and see it yourself.

django-admin-default-logout-page.png

Just as with django.contrib.auth.login() view, we can use a different template by passing template_name keyword argument to django.contrib.auth.logout() view as follows:

url(r'^accounts/logout/$', 
    auth_views.logout, 
    {'template_name': 'blog/logout.html'}, 
    name='logout'
)

We will create custom templates for login and logout page. So modify login and logout URLs to include template name in cadmin's urls.py file as follows:

TGDB/django_project/cadmin/urls.py

#...
urlpatterns = [
    url(r'^accounts/login/$', auth_views.login, {'template_name': 'cadmin/login.html'}, name='login'),
    url(r'^accounts/logout/$', auth_views.logout, {'template_name': 'cadmin/logout.html'}, name='logout'),
    url(r'^post/add/$', views.post_add, name='post_add'),
    #...
]

Before we create login and logout templates, let's create another URL pattern which points to the root URL (i.e http://127.0.0.1:8000/cadmin/) of the cadmin app. Add a URL pattern named home to the urlpatterns list in urls.py file as follows:

TGDB/django_project/cadmin/urls.py

#...
urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^accounts/login/$', auth_views.login, {'template_name': 'cadmin/login.html'}, name='login'),
    #...
]

Then, in the cadmin views.py file add the home() view just below post_update() view as follows:

TGDB/django_project/cadmin/views.py

#...
def post_update(request, pk):
    #...

def home(request):
    if not request.user.is_authenticated():
        return redirect('login')

    return render(request, 'cadmin/admin_page.html')

The only thing that remains now is the templates for the login, logout and admin page. Let's start by creating login template first.

Create a new file named login.html inside cadmin app's templates directory (i.e cadmin/templates/cadmin/) and add the following code to it:

TGDB/django_project/cadmin/templates/cadmin/login.html

{% extends "cadmin/base.html" %}

{% block content %}

    <div class="login">
        <h1>The Great Django Blog - Login</h1>

        <form method="post" action="">
            {% csrf_token %}
            <table>
                {{ form.as_table }}
                <tr>
                    <td>&nbsp;</td>
                    <td><input type="submit" value="Submit"></td>
                </tr>                
            </table>

        </form>

    </div>

{% endblock %}

The value of the form template variable will be provided by django.contrib.auth.login() view using context.

Create another template named logout.html with the following code.

TGDB/django_project/cadmin/templates/cadmin/logout.html

{% extends "cadmin/base.html" %}

{% block content %}

<div class="logout">
        <p>You have successfully logged out. <a href="{% url 'login' %}">Click</a> to login again.</p>
</div>

{% endblock %}

Finally, create admin_page.html and add the following code to it.

TGDB/django_project/cadmin/templates/cadmin/admin_page.html

{% extends "blog/base.html"  %}

{% block title %}
    Blog - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="content">
        <div class="section-inner clearfix">

        <p>Welcome {{ request.user.username }} !</p>

        <p>User Details:</p>

        <ul>
            <li>Email: {{ request.user.username.email|default:"NA" }}</li>
            <li>SuperUser: {{ request.user.is_superuser }}</li>
            <li>Staff: {{ request.user.is_staff }}</li>
            <li>Date Joined: {{ request.user.date_joined }}</li>
            <li>Last Login: {{ request.user.last_login }}</li>
        </ul>

        <p><a href="{% url 'logout' %}">Logout</a></p>

        </div>
    </div>

{% endblock %}

Our login view is almost ready. Visit http://127.0.0.1:8000/cadmin/accounts/login/ and try logging in using wrong username and password. You will be greeted with errors like this:

Selection_017

Try logging in one more time using the correct username and password. If user logged in successfully then the django.contrib.auth.login() view will redirect the user to /accounts/profile/ URL, this is Django's another default setting kicking in.

accounts-profile.png

We don't have any URL pattern in cadmin's urls.py for /accounts/profile/ that's why an HTTP 404 error is displayed.

We can override this setting by adding a hidden field (in login.html) named next with the URL to redirect after login.

<td><input type="hidden" name="next" value="/cadmin/"></td>

This will redirect the user to http://127.0.0.1:8000/cadmin/, after successful login.

Instead of hardcoding the URL. We could also pass the value of the next field using query string like this:

http://127.0.0.1:8000/cadmin/accounts/login/?next=/cadmin/

Open login.html and add the hidden field named next as follows:

TGDB/django_project/cadmin/templates/cadmin/login.html

{# ... #}
<form method="post" action="">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
        <tr>
            <td><input type="hidden" name="next" value="{{ next|default:'/cadmin/' }}"></td>
            <td><input type="submit" value="Submit"></td>
        </tr>
    </table>
</form>
{# ... #}

This is how the above code works:

If we don't provide any value to next form field as query string then the default value i.e /cadmin/ will be used. Otherwise, the user will be redirected to the URL given by the next as a query string.

Visit http://127.0.0.1:8000/cadmin/accounts/login/, enter correct username/password and hit enter. This time you will be redirected to http://127.0.0.1:8000/cadmin/, which looks like this:

cadmin-admin_page.png

To logout click "Logout" link at the bottom of the page or visit http://127.0.0.1:8000/cadmin/accounts/logout/ and you will see a page like this:

Selection_018

Our login and logout system is working as expected, but from the usability point of view, there is still one problem.

Visit http://127.0.0.1:8000/cadmin/accounts/login/ once again and login using the correct username and password. As expected, you will be redirected to http://127.0.0.1:8000/cadmin/. At this point, if we visit http://127.0.0.1:8000/cadmin/accounts/login/ again, Django will redisplay the login the form. Redisplaying login form to a logged in user is absolutely pointless.

We can change this behavior by creating a custom login view function. Open cadmin app's views.py and append the login() view as follows:

TGDB/django_project/cadmin/views.py

#... 
from blog.models import Post, Author, Category, Tag
from django.contrib.auth import views as auth_views
#...

def home(request):
    #...

def login(request, **kwargs):
    if request.user.is_authenticated():
        return redirect('/cadmin/')
    else:
        return auth_views.login(request, **kwargs)

Nothing new here except the additional **kwargs argument. The kwargs enables us to pass the all the extra keyword arguments passed to this view function to the built-in django.contrib.auth.login() view function.

Here is how it works:

The login() view first checks whether the user is authenticated or not. If the user is authenticated then it will redirect him to the admin page. Otherwise, it will display the login form by calling built-in django.contrib.auth.login() view.

Next, we have to update our login URL pattern to use our login() view instead of one provided by Django Authentication framework. Update login URL pattern in urls.py as follows:

TGDB/django_project/cadmin/urls.py

#...
urlpatterns= [
    url(r'^$', views.home, name='home'),
    url(r'^accounts/login/$', views.login, {'template_name': 'blog/login.html'}, name='login'),
    #...
]

Now if you visit http://127.0.0.1:8000/cadmin/accounts/login/ after logging in, you will be redirected to http://127.0.0.1:8000/cadmin/.

Limiting Access #

The whole point of implementing login system to our site is to prevent unauthorized access to administrative pages.

A simple way to restrict access to a page is to first check whether the user is authenticated or not using is_authenticated() method then redirect the user accordingly. For example:

def our_view(request):
    if not request.user.is_authenticated():
        return redirect("login")

    return render(request, 'app/view.html')

We have already employed this technique in the home() and login() view functions.

TGDB/django_project/cadmin/views.py

#...
def home(request):
    if not request.user.is_authenticated():
        return redirect('login')

    return render(request, 'cadmin/admin_page.html')


def login(request, **kwargs):
    if request.user.is_authenticated():
        return redirect('/cadmin/')
    else:
        return auth_views.login(request, **kwargs)

In Django, the preferred way to limit access to pages is to use login_required decorator. To use login_required decorator you must import it from django.contrib.auth.decorators.

Let's update home() view to use login_required decorator as follows:

TGDB/django_project/cadmin/views.py

#...
from django.contrib.auth import views as auth_views
from django.contrib.auth.decorators import login_required

#...
@login_required
def home(request):
    return render(request, 'blog/admin_page.html')
#...

Here is how the login_required decorator work:

If the user is not logged then it will redirect the user to /accounts/login/ (Django's default login URL), passing the current absolute URL as a value to the next query parameter. For example, open your browser and visit http://127.0.0.1:8000/cadmin/ (without logging in). The login_required will redirect you to http://127.0.0.1:8000/accounts/login/?next=/cadmin/ URL.

django-default-login-url.png

On the other hand, if the user is logged in then the login_required would do nothing. In the next section, we are going to change Django's default login URL i.e /accounts/login/.

Updating default Login URL #

In the earlier sections, we have encountered many places where Django uses /accounts/login/ as default URL for login. To change default login URL we use LOGIN_URL setting in settings.py file. LOGIN_URL accepts URL or name of the URL pattern. Open settings.py file and add the following variable at the end of the file.

LOGIN_URL = 'login'

Here we are assigning the value of the name attribute of login URL pattern from cadmin's urls.py. In other words, the above code changes the default login URL from /accounts/login/ to /cadmin/accounts/login/. As a result, if you try to visit a view which has login_required decorator applied to it, you will be redirected to /cadmin/accounts/login/ URL instead of /accounts/login/.

In our case, we want users to let users to login and logout using /cadmin/login/ and /cadmin/logout/ URLs respectively. To do so, open cadmin's urls.py file and make the following changes:

TGDB/django_project/cadmin/urls.py

#...
urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^login/$', views.login, {'template_name': 'cadmin/login.html'}, name='login'),
    url(r'^logout/$', auth_views.logout, {'template_name': 'cadmin/logout.html'}, name='logout'),
    #...
]

As we are using named URL pattern in LOGIN_URL setting we don't need to update anything else. Now our updated login and logout URLs are /cadmin/login/ and /cadmin/logout/ respectively. To verify the changes visit http://127.0.0.1:8000/cadmin/ and you will be redirected to http://127.0.0.1:8000/cadmin/login/?next=/cadmin/.

Selection_019

Enter correct username and password and you will get a page like this:

updated-logout-url.png

Note: To checkout this version of the repository type git checkout 30a.