8. Authentication

Great, we secured our views, but now no one can add new entries to our application. The finishing touch is to implement our authentication views.

Create a sign-in/sign-out form

First we need to add a login form to our existing index.jinja2 template as shown by the emphasized lines.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% extends "layout.jinja2" %}

{% block content %}

    {% if request.authenticated_userid %}
        Welcome <strong>{{request.authenticated_userid}}</strong> ::
        <a href="{{request.route_url('auth',action='out')}}">Sign Out</a>
    {% else %}
        <form action="{{request.route_url('auth',action='in')}}" method="post" class="form-inline">
            <div class="form-group">
                <input type="text" name="username" class="form-control" placeholder="Username">
            </div>
            <div class="form-group">
                <input type="password" name="password" class="form-control" placeholder="Password">
            </div>
            <div class="form-group">
                <input type="submit" value="Sign in" class="btn btn-default">
            </div>
        </form>
    {% endif %}

    {% if paginator.items %}

Now the template first checks if we are logged in. If we are logged in, it greets the user and presents a sign-out link. Otherwise we are presented with the sign-in form.

Update User model

Now it’s time to update our User model.

Lets update our model with two methods: verify_password to check user input with a password associated with the user instance, and by_name that will fetch our user from the database, based on login.

Add the following method to our User class in models/user.py.

19
20
    def verify_password(self, password):
        return self.password == password

We also need to create the UserService class in a new file services/user.py.

1
2
3
4
5
6
7
8
from ..models.user import User


class UserService(object):

    @classmethod
    def by_name(cls, name, request):
        return request.dbsession.query(User).filter(User.name == name).first()

Warning

In a real application, verify_password should use some strong one-way hashing algorithm like bcrypt or pbkdf2. Use a package like passlib which uses strong hashing algorithms for hashing of passwords.

Update views

The final step is to update the view that handles authentication.

First we need to add the following import to views/default.py.

1
2
3
4
5
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget
from ..services.user import UserService
from ..services.blog_record import BlogRecordService

Those functions will return HTTP headers which are used to set our AuthTkt cookie (from AuthTktAuthenticationPolicy) in the user’s browser. remember is used to set the current user, whereas “forget” is used to sign out our user.

Now we have everything ready to implement our actual view.

18
19
20
21
22
23
24
25
26
27
28
29
30
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
    username = request.POST.get('username')
    if username:
        user = UserService.by_name(username, request=request)
        if user and user.verify_password(request.POST.get('password')):
            headers = remember(request, user.name)
        else:
            headers = forget(request)
    else:
        headers = forget(request)
    return HTTPFound(location=request.route_url('home'), headers=headers)

This is a very simple view that checks if a database row with the supplied username is present in the database. If it is, a password check against the username is performed. If the password check is successful, then a new set of headers (which is used to set the cookie) is generated and passed back to the client on redirect. If the username is not found, or if the password doesn’t match, then a set of headers meant to remove the cookie (if any) is issued.

Current state of our application

For convenience here are the files you edited in their entirety (services/user.py was already rendered above).

templates/index.jinja2

 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 "layout.jinja2" %}

{% block content %}

    {% if request.authenticated_userid %}
        Welcome <strong>{{request.authenticated_userid}}</strong> ::
        <a href="{{request.route_url('auth',action='out')}}">Sign Out</a>
    {% else %}
        <form action="{{request.route_url('auth',action='in')}}" method="post" class="form-inline">
            <div class="form-group">
                <input type="text" name="username" class="form-control" placeholder="Username">
            </div>
            <div class="form-group">
                <input type="password" name="password" class="form-control" placeholder="Password">
            </div>
            <div class="form-group">
                <input type="submit" value="Sign in" class="btn btn-default">
            </div>
        </form>
    {% endif %}

    {% if paginator.items %}

        <h2>Blog entries</h2>

        <ul>
            {% for entry in paginator.items %}
                <li>
                    <a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">
                        {{ entry.title }}
                    </a>
                </li>
            {% endfor %}
        </ul>

        {{ paginator.pager() |safe }}

    {% else %}

        <p>No blog entries found.</p>

    {% endif %}

    <p><a href="{{ request.route_url('blog_action',action='create') }}">
        Create a new blog entry</a></p>

{% endblock %}

models/user.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import datetime #<- will be used to set default dates on models
from pyramid_blogr.models.meta import Base  #<- we need to import our sqlalchemy metadata from which model classes will inherit
from sqlalchemy import (
    Column,
    Integer,
    Unicode,     #<- will provide Unicode field
    UnicodeText, #<- will provide Unicode text field
    DateTime,    #<- time abstraction field
)


class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(Unicode(255), unique=True, nullable=False)
    password = Column(Unicode(255), nullable=False)
    last_logged = Column(DateTime, default=datetime.datetime.utcnow)

    def verify_password(self, password):
        return self.password == password

views/default.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 pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget
from ..services.user import UserService
from ..services.blog_record import BlogRecordService


@view_config(route_name='home',
             renderer='pyramid_blogr:templates/index.jinja2')
def index_page(request):
    page = int(request.params.get('page', 1))
    paginator = BlogRecordService.get_paginator(request, page)
    return {'paginator': paginator}


@view_config(route_name='auth', match_param='action=in', renderer='string',
             request_method='POST')
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
    username = request.POST.get('username')
    if username:
        user = UserService.by_name(username, request=request)
        if user and user.verify_password(request.POST.get('password')):
            headers = remember(request, user.name)
        else:
            headers = forget(request)
    else:
        headers = forget(request)
    return HTTPFound(location=request.route_url('home'), headers=headers)

Voilà!

You can now sign in and out to add and edit blog entries using the login admin with password admin (this user was added to the database during the initialize_db step). But we have a few more steps to complete this project.

Next: 9. Registration.