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 accetps 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='password')
>>> 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. This is the responsiliblity 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.

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

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

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('login')

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

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

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 %}    

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 'login' %}">Click here</a> to login again.</p>

        </div>
    </div>

{% endblock %}

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 %}

Finally, add the following url patterns to the blog's urls.py file:

urlpatterns = [
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),
    url(r'^admin_page/$', views.admin_page, name='admin_page'),
    ...
]    

Visit http://127.0.0.1:8000/login/ and enter correct username and password and you will be greeted with a page like this:

[]

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

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

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 create these two url patterns in the cadmin app's urls.py file as follows:

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:

[img]

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.

[img]

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 should look like this:

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:

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 as follows:

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

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

Notice that we are using admin_page.html template from the blog app.

The only thing remains now is the login and logout 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:

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 file named logout.html with the following code.

{% 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 %}

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

[img]

Try loggin 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.

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>

After login this code will redirect the user to http://127.0.0.1:8000/cadmin/.

Instead of hardcoding the url. We could also pass value of next field as a GET pameter like this:

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

Open login.html and modify the file as follows:

<form action="" method="post">
    {% 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 GET parameter then the default value i.e /cadmin/ will be used. Otherwise, the user will be redirected to the URL given by the next GET parameter.

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/. As /cadmin/ URL is not mapped to any url pattern you would get a HTTP 404 error on visiting http://127.0.0.1:8000/cadmin/ , w'll fix it later. To logout visit http://127.0.0.1:8000/cadmin/accounts/logout/ and you will see a page like this:

[img]

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 easily change this behavior by creating a custom login view which first checks whether the user is authenticated or not. If user is authenticated the redirect him to admin page, otherwise display login form by calling built-in login() view.

Open cadmin app's views.py in cadmin app and add the following login() view as follows:

... 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 to 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:

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, 'blog/admin_page.html')

Another common and more preffered way to limit access to pages is to use login_required decorator.
To use login_required decorator amend home() view in views.py file as follows:

...
from django.contrib.auth.decorators import login_required

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

Notice that before using login_required decorator in views you must import it from django.contrib.auth.decorators.

Here is how login_required decorators works:

If user is not logged then it will redirect the user to /accounts/login/, passing path of the URL in the query string as next. For example, if you try to visit http://127.0.0.1:8000/cadmin/ (without logging in), then login_required will redirect you to http://127.0.0.1:2000/accounts/login/?next=/cadmin/.

On the other hand if user is logged in then view would execute as usual. In the next section we will learn how 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 name attribute of login url pattern. From now on, if you try to visit a view which has login_required decorator applied to it, you will be redirected to url pattern named login.

We are not bound to use /accounts/login/ and /accounts/logout/ as login and logout URLs. In our case we want 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 urls.py file and make the following changes:

urlpatterns = [
    ...
    url(r'^login/$', auth_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 is http://127.0.0.1:8000/cadmin/login/ and http://127.0.0.1:8000/cadmin/logout/.