Django Logging Users In and Out

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

authenticate() and login() functions #

Django authentication framework (django.contrib.auth) provides authenticate() and login() function 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 is 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 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.contrib import auth
...

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'),
    ...
]

Visit http://127.0.0.1:8000/login/ and 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, first add these two URL patterns in the cadmin's app urls.py file as follows:

TGDB/django_project/cadmin/urls.py

...
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'),
    ..
]

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

Save the above configuration and visit http://127.0.0.1:8000/cadmin/accounts/login/ and you would get TemplateDoesNotExist exception as follows:

templatedoesnotexist-image.png

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

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

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

Similarly, by default the logout() view uses registration/logged_out.html template from the admin app. 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 login() view, we can use a different template by passing template_name keyword argument as follows:

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

At this point, login and logout URL pattern in cadmin's urls.py should look like this:

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'),
    ...
]

Before we create login and logout templates, let's create another URL pattern which points to the root of the cadmin app i.e http://127.0.0.1:8000/cadmin/. Add the 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 home(request):
    if not request.user.is_authenticated():
        return redirect('login')

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

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

Create a new file named login.html inside cadmin app 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 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:

cadmin-login-page.png

Try logging in one more time using correct username and password. If user logged in successfully then login() will redirect the user to /accounts/profile/ i.e http://127.0.0.8000/accounts/profile/, 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 404 error is displayed.

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

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

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

Instead of hardcoding the URL. We could also pass value of 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 query string.

Visit http://127.0.0.1:8000/cadmin/accounts/login/, enter correct username/password and hit enter. This time you will be redirect 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:

cadmin-logout-page.png

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/password. As expected, you will be redirect 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. Redisplying 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 django.contrib.auth import views as auth_views

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

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

Here is how it works:

The login() view first checks whether the user is authenticated or not. If user is authenticated then it will redirect him to the admin page. Otherwise, it will display the login form by calling built-in 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 as follows:

TGDB/django_project/cadmin/urls.py

...
urlpatterns= [
    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')

Recall that we have already employed this technique in our home() view function.

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

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

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.decorators import login_required

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

Here is how login_required decorators works:

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

django-default-login-url.png

On the other hand, if user is logged in then 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 named 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 http://127.0.0.1:8000/cadmin/accounts/login/ instead of http://127.0.0.1:8000/accounts/login/.

In our case, we want users to let users to login and logout using http://127.0.0.1:8000/cadmin/login/ and http://127.0.0.1:8000/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'^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 http://127.0.0.1:8000/cadmin/login/ and http://127.0.0.1:8000/cadmin/logout/ respectively. To verify the changes visit http://127.0.0.1:8000/cadmin/ and you will redirected to http://127.0.0.1:8000/cadmin/login/?next=/cadmin/.

updated-login-url.png

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

updated-logout-url.png