Django Authorisation

Authorisation

This post is part of my Django series. You can see an overview of the series along with instruction on how to get all the source code here.

This article assumes you are comfortable creating a Django project, can create apps and register them into the INSTALLED_APPS list of the settings file. If not please read my Django HelloWorld article.

This article assumes you have a project called DjangoSandBox in an application called authmodel.

Prerequisites

Django provides authentication functionality through apps and middle ware.

Within the INSTALLED_APPS list of the settings.py add the following:

  • ‘django.contrib.auth’
  • ‘django.contrib.contenttypes’

Within the MIDDLEWARE_CLASSES list of the settings.py add the following:

  • SessionMiddleware
  • AuthenticationMiddleware
  • SessionAuthenticationMiddleware

If they were not added or you are starting a new project ensure to run migrate to create the required database schema and run any required configuration scripts:

python manage.py migrate

**Note: ** by default these apps and middle ware settings are already added into the settings.py

The User Model

By default the user class is django.contrib.auth.models.User and is found within the auth_user table. It contains many fields including the following; username, password, email, first_name, last_name

To create a user:

new_user = User.objects.create_user(username="user_login", email="foo@email.com", password="password", first_name="first_name", last_name="last_name")

By default the user is not created as active; it is presumed we would like some type of email confirmation from the user before activating the account. We can make the user active with the following code:

new_user.is_active = True
new_user.save()

We can reset their password with the set_password function. It is important that we use this method and not set the field directly to ensure their password is encrypted. Setting the field directly would mean their password would be stored in plain text which would not be secure.

new_user.set_password("new_pw")
new_user.save()

Logging in a user is a two step process. Initially we need to ensure that their username and password are valid, if this is the case then we log them in with the login function.

user = authenticate(username=username, password=password)

if user:
    login(request, user)

Django handles everything else; we simply need to tell Django which view functions require an authenticated user.

Django uses sessions to add the authentication into the request object.

The user template variable or the request.user property can be used to access the logged in user. If the user is not logged in then an instance of AnonymousUser is returned. The function user.is_authenticated() can be used to determine if the user is logged in:

if request.user.is_authenticated():
    # Authenticated 
else:
    # Anonymous 

Enforcing Authorisation

First lets make a view with some sensitive information. The following returns all user records:

#views.py

from django.contrib.auth.models import User
from django.shortcuts import render

def users(request):
    return render(request, 'authmodel/users.html', {"users": User.objects.all()})

To enforce a user to log in we wrap the view function with the login_required function in the URL routing config of our authmodel application:

# urls.py

from django.conf.urls import patterns, url
from django.contrib.auth.decorators import login_required

from . import views

url(r'^users/$', login_required(views.users), name="users"),

We need to hook in our authmodel application URL routing config into our project:

from django.conf.urls import patterns, include, url
from django.contrib import admin

from authmodel import urls as authmodel_urls

urlpatterns = 
    patterns('',
             url(r'^auth/', include(authmodel_urls, namespace="auth")),
             )

Now if a user tries to access http://127.0.0.1:8000/auth/users and they are not logged in they are redirected to the defined login URL which we set in the LOGIN_URL variable of our settings.py. We will fix this later on.

An alternative to using the login_require function in our URL routing config is to decorate our view function with the @login_required decorator:

#views.py

from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.shortcuts import render

from .forms import UserForm, UserLoginForm

@login_required
def users(request):
    return render(request, 'authmodel/users.html', {"users": User.objects.all()})

We can now remove the login_require function wrap within our url routing config:

#urls.py

from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
             url(r'^users/$', views.users, name="users"),
             )

Here is our view which displays information about the users passed in to the context data as the template variable named users:

<!--templates/authmodel/users.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Registered Users</title>
</head>
<body>
<h2>Registered Users</h2>

<table>
    <tr>
        <th>First Name</th>
        <th>Last Name</th>
        <th>Email</th>
        <th>User Name</th>
    </tr>
    {% for user in users %}
        <tr>
            <td>{{ user.first_name }}</td>
            <td>{{ user.last_name }}</td>
            <td>{{ user.email }}</td>
            <td>{{ user.username }}</td>
        </tr>
    {% endfor %}
</table>

<span>
    <a href="{% url 'auth:index' %}">Back</a>
</span>
</body>
</html>

We now need to create pages for logging in and out as well as the ability to create a new login.

Create Login

We are going to create a login form which matches the data we want to collect from the user when they create a login.

We override the clean function of the form to validate that the password and the password_confirm are identical.

forms.py
from django.forms import Form, CharField, EmailField, PasswordInput, ValidationError

class UserForm(Form):
    first_name = CharField(max_length=30)
    last_name = CharField(max_length=30)
    login = CharField()
    email = EmailField()
    password = CharField(widget=PasswordInput)
    password_confirm = CharField(widget=PasswordInput)

    def clean(self):
        super(UserForm, self).clean()

        password = self.cleaned_data.get('password')
        password_confirm = self.cleaned_data.get('password_confirm')

        if password and password_confirm and password != password_confirm:
            raise ValidationError("Passwords do not match")

        return self.cleaned_data

We now hook this form into a function view; here we create a new user after the form has been posted and validates correctly.

Once we create the user we activate them. In a live system we might want to think about sending and receiving a confirmation email before we activate the user.

#views.py
def create_login(request):
    form = UserForm(request.POST or None)

    if request.POST and form.is_valid():
        email = form.cleaned_data['email']
        user_login = form.cleaned_data['login']
        password = form.cleaned_data['password']
        last_name = form.cleaned_data['last_name']
        first_name = form.cleaned_data['first_name']

        new_user = User.objects.create_user(username=user_login, email=email, password=password, first_name=first_name, last_name=last_name)

        new_user.is_active = True
        new_user.save()   

        return redirect(reverse('auth:index'))

    return render(request, 'authmodel/create_login.html', {'form': form})

A simple template which renders the required inputs via the form we have created above.

<!-- templates/authmodel/create_login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Create Login</title>
</head>
<body>
<h1>Create Login</h1>

<form method="post" action="">
    {% csrf_token %}
    <table>{{ form.as_table }}</table>
    <p><input type="submit" value="Create"/></p>
</form>

<span>
    <a href="{% url 'auth:index' %}">Back</a>
</span>
</body>
</html>

Adding our new function into the url routing config:

#urls.py

from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
             url(r'^login/create/$', views.create_login, name="login_create")
             )

To test run the development server and navigate to:

http://127.0.0.1:8000/auth/login/create/

Login

Our login page simply requires a text box for the username and password along with the ability to post this data back to the server.

First we create a form to represent the data required; username and password:

#forms.py
from django.forms import Form, CharField, EmailField, PasswordInput, ValidationError

class UserLoginForm(Form):
    login = CharField()
    password = CharField(widget=PasswordInput)

We then create a view function around the form we have just created.

If Django redirected the user to this page upon accessing a page which required authentication; the original URL will be available via the NEXT get query parameter. We will redirect the user back to this page if applicable.

#views.py

from django.contrib.auth import authenticate, login
from django.shortcuts import render, redirect

from .forms import UserLoginForm

def login_page(request):
    form = UserLoginForm(request.POST or None)

    if request.POST and form.is_valid():
        username = form.cleaned_data["login"]
        password = form.cleaned_data["password"]

        user = authenticate(username=username, password=password)

        if user:
            login(request, user)

            if request.GET.get('next') is not None:
                return redirect(request.GET['next'])
            else:
                return redirect(reverse('auth:index'))

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

Now we create the template which renders the user login form.

When a user is logged into any page we have access to their user details via the template variable called user. We use the user.is_authenticated property to determine if someone is logged in already.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
    <style>
        span {
            display: block;
        }
    </style>
</head>
<body>

<h1>Login</h1>

{% if user.is_authenticated %}
    <span>{{ user.first_name }} {{ user.last_name }} you are already logged in!</span>
{% else %}
    <form method="post" action="">
        {% csrf_token %}
        <table>{{ form.as_table }}</table>
        <input type="submit" class="button" value="Login"/>
    </form>
{% endif %}

<span>
    <a href="{% url 'auth:index' %}">Back</a>
</span>
</body>
</html>

Adding in our new function into our URL routing config:

#urls.py

from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
             url(r'^login/$', views.login_page, name="login"),
             )

Add our login URL into our settings file. Any attempt to access a page which requires authorisation will redirect here automatically if a user is not logged in.

#settings.py
LOGIN_URL = '/auth/login/'

To test run the development server and navigate to:

http://127.0.0.1:8000/auth/login/

Logout

We can log a user out by passing the request object to the logout function. We do this inside a view function:

#views.py

from django.contrib.auth import logout
from django.shortcuts import render


def logout_page(request):
    logout(request)
    return render(request, 'authmodel/logout.html')

A simple page which confirms to the user that they are now logged out:

<!--templates/authmodel/logout.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Logout</title>
    <style>
        span {
            display: block;
        }
    </style>

</head>
<body>
<h1>Logout</h1>

<span>You are now logged out</span>

<span>
    <a href="{% url 'auth:index' %}">Back</a>
</span>
</body>
</html>

Hooking in our logout page into our URL routing config:

#urls.py

from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
             url(r'^logout/$', views.logout_page, name="logout"),
             )

To test run the development server and navigate to:

http://127.0.0.1:8000/auth/login/logout/

Extending User

If the default User profile is not suitable for your purposes there are a couple options.

Firstly you can create your own table with the additional fields and create a join to the existing user table, accessing it via ettings.AUTH_USER_MODEL:

# models.py
user = models.OneToOneField(settings.AUTH_USER_MODEL)

Alternatively you can subclass either AbstractUser or BaseUserManager. If you go down this route you will need to register the new table as replacing the default Djang Auth.User model table:

#settings.py
AUTH_USER_MODEL = "authapp.NewAuthUser"

References

https://docs.djangoproject.com/en/1.8/topics/auth/
https://docs.djangoproject.com/en/1.8/topics/auth/default/
https://docs.djangoproject.com/en/1.8/topics/auth/default/#permissions-and-authorization

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s