OverIQ.com

Django Logging Users In and Out

Last updated on July 27, 2020


Django provides built-in URL patterns and view functions for logging users in and out. But before we add them to our project, we will create login and logout system on our own 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>>
>>> from django.contrib import auth
>>>
>>> user = auth.authenticate(username='admin', password='passwordd')
>>>
>>> user
<User: admin>
>>>
>>> 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 the user we use login() function. It takes two arguments, request object (HttpRequest) and a User object. It works by saving 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 and cookie 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 already logged in.

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

Creating a Login System #

In djangobin app's views.py file , add login, logout and user_details views towards the end of the file as follows:

djangobin/django_project/djangobin/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#...
from django.core.mail import mail_admins
from django.contrib.auth.models import User
from django.contrib import auth
import datetime
from .forms import SnippetForm, ContactForm
from .models import Language, Snippet, Tag
from .utils import paginate_result

#...

def profile(request):
    #...


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

    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('djangobin:user_details')

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

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


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


def user_details(request):    
    user = get_object_or_404(User, id=request.user.id)    
    return render(request, 'djangobin/user_details.html', {'user': user})

Then create three templates login.html, logout.html and user_details.html with the following code:

djangobin/django_project/djangobin/templates/djangobin/login.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{% extends "djangobin/base.html"  %}

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

{% block main %}

    <div class="row">
        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Login</h4>
            <hr>

            {% if messages %}
                {% for message in messages %}
                    <p class="alert alert-info">{{ message }}</p>
                {% endfor %}
            {% endif %}
            
                <form method="post">

                    {% csrf_token %}

                    <table class="table">
                        <tr>
                            <th><label for="id_username">Username:</label></th>
                            <td><input type="text" name="username" id="id_username" required /></td>
                        </tr>
                        <tr>
                            <th><label for="id_password">Password:</label></th>
                            <td><input type="password" name="password" id="id_password" required /></td>
                        </tr>
                        <tr>
                            <td><input type="hidden" name="next" value=""></td>
                            <td><button type="submit" class="btn btn-primary">Submit</button></td>
                        </tr>
                    </table>

                </form>            
        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="/password-reset/">Forgot Password?</a> <br>
                <a href="/register/">Create new account.</a> <br>
                <a href="/contact/">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

djangobin/django_project/djangobin/templates/djangobin/logout.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{% extends "djangobin/base.html"  %}

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

{% block main %}

    <div class="container">

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

    </div>

{% endblock %}

djangobin/django_project/djangobin/templates/djangobin/user_details.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{% extends 'djangobin/base.html' %}

{% block title %}
    User Details - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">

        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Account Details</h4>

            <hr>

            <dl class="dl-horizontal">
                <dt>Username</dt>
                <dd>{{ request.user.username }}</dd>

                <dt>Email</dt>
                <dd>{{ request.user.email }}</dd>

                <dt>Date Joined</dt>
                <dd>{{ request.user.date_joined }}</dd>

                <dt>Last Login</dt>
                <dd>{{ request.user.last_login }}</dd>

                <dt>Snippet created</dt>
                <dd>{{ request.user.profile.get_snippet_count }}</dd>
            </dl>

        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="">My Pastes</a> <br>
                <a href="">Settings</a> <br>
                <a href="">Change Password.</a> <br>
                <a href="{% url 'djangobin:logout' %}">Logout.</a> <br>
            </p>
        </div>

    </div>

{% endblock %}

Nothing extraordinary here, we are just using some of the attributes we have learned in the chapter Django Authentication Framework Basics to get some information about the logged in user.

Add links to login and logout page in base.html file as follows:

djangobin/django_project/djangobin/templates/djangobin/base.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{# ... #}
                    {% if request.user.is_authenticated %}
                        <ul class="dropdown-menu">
                            <li><a href="">My Pastes</a></li>
                            <li><a href="">Account Details</a></li>
                            <li><a href="">Settings</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="{% url 'djangobin:logout' %}">Logout</a></li>
                        </ul>
                    {% else %}
                        <ul class="dropdown-menu">
                            <li><a href="">Sign Up</a></li>
                            <li><a href="{% url 'djangobin:login' %}">Login</a></li>
                        </ul>
                    {% endif %}
                </li>
            </ul>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>


<div class="container">

    <div class="row">

        <div class="col-lg-9 col-md-9">

            {% if not request.user.is_authenticated and not request.path == '/login/'  %}
                <p class="alert alert-info">
                    <a href="{% url 'djangobin:login' %}" class="alert-link">Login</a> to access other cool features.
                </p>
            {% endif %}


            {% block main %}
                {#  override this block in the child template  #}
            {% endblock %}

        </div>
{# ... #}

Finally, add the following three URL patterns in the djangobin app's urls.py file:

djangobin/django_project/djangobin/urls.py

1
2
3
4
5
6
7
8
#...
urlpatterns = [
    #...
    url('^contact/$', views.contact, name='contact'),
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),
]

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

Enter bogus username and password and you will get errors like this:

Now enter correct username and password and you will be redirected to user detail page:

To logout, click the logout link at the right side of the page. And you should see logout page like this:

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

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

To use these views, import them from django.contrib.auth package and update login and logout URL patterns in the urls.py file as follows:

djangobin/django_project/djangobin/urls.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#...
from django.contrib.auth import views as auth_views
from . import views

# app_name = 'djangobin'

urlpatterns = [
    #...
    url(r'^login/$', auth_views.login, name='login'),
    url(r'^logout/$', auth_views.logout, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),
]

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

The problem is that by default, the django.contrib.auth.login() view looks for a template called registration/login.html. However, Django doesn't provide this template that's why a TemplateDoesNotExist exception is raised.

Also, notice the Template-loader postmortem section. It tells you the exact order in which Django tried to find the template.

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

1
2
3
4
5
url(r'^login/$',
    auth_views.login, 
    {'template_name': 'djangobin/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/logout/ and see it yourself.

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:

1
2
3
4
5
url(r'^logout/$', 
    auth_views.logout, 
    {'template_name': 'djangobin/logout.html'}, 
    name='logout'
)

Modify login and logout URL patterns to use a custom templates as follows:

djangobin/django_project/djangobin/urls.py

1
2
3
4
5
6
7
8
#...

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

Next, update login.html template to use form template variable provided by django.contrib.auth.login() view as follows:

djangobin/django_project/djangobin/templates/djangobin/login.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{% extends "djangobin/base.html"  %}

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

{% block main %}

    <div class="row">
        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Login</h4>
            <hr>

            {% if messages %}
                {% for message in messages %}
                    <p class="alert alert-info">{{ message }}</p>
                {% endfor %}
            {% endif %}

            <form method="post">

                {% csrf_token %}

                <table class="table">
                    {{ form.as_table }}
                    <tr>
                        <td>&nbsp;</td>
                        <td><button type="submit" class="btn btn-primary">Submit</button></td>
                    </tr>
                </table>

            </form>
        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="/password-reset/">Forgot Password?</a> <br>
                <a href="/register/">Create new account.</a> <br>
                <a href="#">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

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

Try logging in one more time using the correct username and password. On success, you will redirect you to /accounts/profile/ URL. This is another default behavior of django.contrib.auth.login() view.

We don't have any URL pattern in djangobin's urls.py to match /accounts/profile/ URL path, that's why the server returned HTTP 404 error.

We can easily override this behavior using the LOGIN_REDIRECT_URL setting. Open settings.py file and add LOGIN_REDIRECT_URL towards the end of the file as follows:

djangobin/django_project/django_project/settings.py

1
2
3
4
5
6
#...
MANAGERS = (
    ('OverIQ', 'manager@overiq.com'),
)

LOGIN_REDIRECT_URL = 'djangobin:index'

This will change the redirect URL from /accounts/profile/ to /

Instead of passing name of the URL pattern, we can also pass URL path directly.

From now on, after successfull login, django.contrib.auth.login() view will redirect the user to / URL path instead of /accounts/profile/.

But there are still some limitations to this. For example, let's say you were browsing trending snippets and then you decided to login. After logging in, it makes more sense to redirect you to the trending page again instead of the / URL.

To make this happen, we can embed a hidden field named next containing the URL to redirect to after logging in.

When the django.contrib.auth.login() view receives next as POST data, it redirects to the URL specied in the hidden next field.

The django.contrib.auth.login() view also provides a context variable called next, which contains the URL where useres will be redirected after logging in. The value of the next variable will be either /accounts/profile/ or the URL specified in LOGIN_REDIRECT_URL variable.

We specify the value of the next field using query string like this:

http://127.0.0.1:8000/login/?next=/trending/

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

djangobin/django_project/djangobin/templates/djangobin/login.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{# ... #}
            <form method="post">

                {% csrf_token %}

                <table class="table">
                    {{ form.as_table }}
                    <tr>
                        <td><input type="hidden" name="next" value="{{ next }}"></td>
                        <td><button type="submit" class="btn btn-primary">Submit</button></td>
                    </tr>
                </table>

            </form>
{# ... #}

This is how the above code works:

If we visit login page using http://localhost:8000/login/ URL, then after logging in django.contrib.auth.login() will redirect the user to / URL. On the other hand, if we visit login page, using http://127.0.0.1:8000/login/?next=/trending/ URL, then the django.contrib.auth.login() view will redirect the user to /trending/ URL.

Next, modify base.html to provide a value to next query parameter as follows:

djangobin/django_project/djangobin/templates/djangobin/base.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{# ... #}
<div class="container">

    <div class="row">

        <div class="col-lg-9 col-md-9">

            {% if not request.user.is_authenticated and not request.path == '/login/'  %}
                <p class="alert alert-info">
                    <a href="{% url 'djangobin:login' %}?next={{ request.path }}" class="alert-link">Login</a> to access other cool features.
                </p>
            {% endif %}
{# ... #}

Let's test whether everything is working or not.

If you are already logged in, logout first by visiting http://localhost:8000/logout/ URL directly or by clicking the Logout link at the top right corner of the page.

Then, navigate to the login page (http://localhost:8000/login/), enter correct username and password. On success, you will be redirected to the index page of djangobin:

Logout again and navigate to the login page again by clicking "Login" link in the trending snippet page. This time after logging in you will be redirected to /trending/ instead of / URL.

Logging in using Email and Password #

As you have seen, by default Django requires you to enter username and password to login into the application. If you deliberately want this behavior that's fine. However, Just to show you how you can take an alternative route, our djangobin application will use email and password to authenticate users. To accomplish this task, we will create a custom form and view function.

Open forms.py and add LoginForm class as follows:

djangobin/django_project/djangobin/forms.py

1
2
3
4
5
6
7
#...
class ContactForm(forms.Form):
    #...

class LoginForm(forms.Form):
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

Next, modify login() view function to use LoginForm as follows:

djangobin/django_project/djangobin/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#...
from .forms import SnippetForm, ContactForm, LoginForm
#...


def login(request):
    if request.method == 'POST':

        f = LoginForm(request.POST)
        if f.is_valid():

            user = User.objects.filter(email=f.cleaned_data['email'])

            if user:
                user = auth.authenticate(
                    username=user[0].username,
                    password=f.cleaned_data['password'],
                )

                if user:
                    auth.login(request, user)
                    return redirect( request.GET.get('next') or 'djangobin:index' )

            messages.add_message(request, messages.INFO, 'Invalid email/password.')
            return redirect('djangobin:login')

    else:
        f = LoginForm()

    return render(request, 'djangobin/login.html', {'form': f})

In line 12, we are checking whether any user associated with the submitted email exists or not.

If the user exists, In line 15, we are authenticating it using the authenticate() function. Notice that the arguments passed to authenticate() function are still username and password.

If the authentication is successful, we login the user using the login() function and redirect it.

Update login and logout URL patterns in urls.py file to use login() and logout function of views.py file as follows:

djangobin/django_project/djangobin/urls.py

1
2
3
4
5
6
7
#...
urlpatterns = [
    #...
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),
]

Visit login page and enter incorrect email and password. You will get an error like this:

Now, enter correct email and password and you will be redirected to the index page.

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

The issue is that the login form is still visible to the logged in user.

Displaying login form to a logged in user is absolutely pointless. To fix the issue simply check whether the user is logged in or not at the start of login() view function as follows:

djangobin/django_project/djangobin/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#...

def login(request):

    if request.user.is_authenticated:
        return redirect('djangobin:profile', username=request.user.username)

    if request.method == 'POST':

        f = LoginForm(request.POST)
        if f.is_valid():

If you now visit http://localhost:8000/login/, after logging in, you will be redirected to the user profile page.

Currently, user profile page just displays the name of the user. We will update it to display a list of snippets in the upcoming lessons.

Limiting Access #

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

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

1
2
3
4
5
def our_view(request):
    if not request.user.is_authenticated():
        return redirect("login")

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

We can copy and paste this condition at the start of the every administrative view function. This will work but Django provides a much better way.

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 module.

Let's update user_details and logout view to use login_required decorator as follows:

djangobin/django_project/djangobin/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#...
from django.contrib import auth
from django.contrib.auth.decorators import login_required
import datetime
from .forms import SnippetForm, ContactForm, LoginForm
#...

#...

@login_required
def logout(request):
    auth.logout(request)
    return render(request,'djangobin/logout.html')


@login_required
def user_details(request):
    user = get_object_or_404(User, id=request.user.id)
    return render(request, 'djangobin/user_details.html', {'user': user})

Here is how the login_required decorator work:

If the user is not logged then it will redirect the user to /accounts/login/ (the default login URL), passing the current absolute URL as a value to the next query parameter. On the other hand, if the user is logged in then the login_required would do nothing.

To change default login URL we use LOGIN_URL setting. The LOGIN_URL accepts URL path or name of the URL pattern. Open settings.py file and add the following variable at the end of the file.

djangobin/django_project/django_project/settings.py

1
2
3
4
5
#...

LOGIN_REDIRECT_URL = 'djangobin:index'

LOGIN_URL = 'djangobin:login'

This changes the default login from /accounts/login/ to /login/. If you try to visit a view which has login_required decorator applied to it, you will be redirected to /login/ URL instead of /accounts/login/.

To verify the changes visit http://localhost:8000/userdetails/ URL and you will be redirected to http://localhost:8000/login/?next=/userdetails/.