9. Registration

Now we have a basic functioning application, but we have only one hardcoded administrator user that can add and edit blog entries. We can provide a registration page for new users, too.

Then we need to to provide a quality hashing solution so we can store secure password hashes instead of clear text. This functionality will be provided by passlib.

Create the registration class, route, and form

We should create a form to handle registration requests. Let’s open forms.py at the root of our project, and edit an import at the top of the files and add a new form class at the end as indicated by the emphasized lines.

1
2
from wtforms import Form, StringField, TextAreaField, validators
from wtforms import IntegerField, PasswordField
17
18
19
20
class RegistrationForm(Form):
    username = StringField('Username', [validators.Length(min=1, max=255)],
                           filters=[strip_filter])
    password = PasswordField('Password', [validators.Length(min=3)])
Our second step will be adding a new route that handles user registration in
routes.py file as shown by the emphasized line.
7
8
    config.add_route('auth', '/sign/{action}')
    config.add_route('register', '/register')

We should add a link to the registration page in our templates/index.jinja2 template so we can easily navigate to it as shown by the emphasized line.

19
20
21
        </form>
        <a href="{{request.route_url('register')}}">Register here</a>
    {% endif %}

Create the registration view

At this point we have the form object and routing set up. We are missing a related view, model, and template code. Let us move forward with the view code in views/default.py.

First we need to import our form definition user model at the top of the file as shown by the emphasized lines.

5
6
7
from ..services.blog_record import BlogRecordService
from ..forms import RegistrationForm
from ..models.user import User

Then we can start implementing our view logic by adding the following lines to the end of the file.

34
35
36
37
38
39
@view_config(route_name='register',
             renderer='pyramid_blogr:templates/register.jinja2')
def register(request):
    form = RegistrationForm(request.POST)
    if request.method == 'POST' and form.validate():
        new_user = User(name=form.username.data)
40
41
42
        request.dbsession.add(new_user)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form}

Next let’s create a new registration template called templates/register.jinja2 with the following content.

 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
{% extends "pyramid_blogr:templates/layout.jinja2" %}

{% block content %}
    <h1>Register</h1>

    <form action="{{request.route_url('register')}}" method="post" class="form">

        {% for error in form.username.errors %}
            <div class="error">{{ error }}</div>
        {% endfor %}

        <div class="form-group">
            <label for="title">{{form.username.label}}</label>
            {{form.username(class_='form-control')}}
        </div>

        {% for error in form.password.errors %}
            <div class="error">{{error}}</div>
        {% endfor %}

        <div class="form-group">
            <label for="body">{{form.password.label}}</label>
            {{form.password(class_='form-control')}}
        </div>
        <div class="form-group">
            <label></label>
            <button type="submit" class="btn btn-default">Submit</button>
        </div>


    </form>
    <p><a href="{{request.route_url('home')}}">Go Back</a></p>
{% endblock %}

Hashing passwords

Our users can now register themselves and are stored in the database using unencrypted passwords (which is a really bad idea).

This is exactly where passlib comes into play. We should add it to our project’s requirements in setup.py at the root of our project.

requires = [
    ...
    'paginate==0.5.6', # pagination helpers
    'paginate_sqlalchemy==0.2.0',
    'passlib'
]

Now we can run either command pip install passlib or python setup.py develop to pull in the new dependency of our project. Password hashing can now be implemented in our User model class.

We need to import the hash context object from passlib and alter the User class to contain new versions of methods verify_password and set_password. Open models/user.py and edit it as indicated by the emphasized lines.

11
12
13
14
15
16
17
18
19
20
21
from passlib.apps import custom_app_context as blogger_pwd_context


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):
22
23
24
25
26
        return blogger_pwd_context.verify(password, self.password)

    def set_password(self, password):
        password_hash = blogger_pwd_context.encrypt(password)
        self.password = password_hash

The last step is to alter our views/default.py to set the password, as shown by the emphasized lines.

40
41
42
43
        new_user.set_password(form.password.data.encode('utf8'))
        request.dbsession.add(new_user)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form}

Now our passwords are properly hashed and can be securely stored.

If you tried to log in with admin/admin credentials, you may notice that the application threw an exception ValueError: hash could not be identified because our old clear text passwords are not identified. So we should allow our application to migrate to secure hashes (usually strong sha512_crypt if we are using the quick start class).

We can easly fix this by altering our verify_password method in models/user.py.

21
22
23
24
25
26
    def verify_password(self, password):
        # is it cleartext?
        if password == self.password:
            self.set_password(password)

        return blogger_pwd_context.verify(password, self.password)

Keep in mind that for proper migration of valid hash schemes, passlib provides a mechanism you can use to quickly upgrade from one scheme to another.

Current state of our application

For convenience here are the files you edited in their entirety, with edited lines emphasized. Files already rendered in their entirety are omitted.

forms.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from wtforms import Form, StringField, TextAreaField, validators
from wtforms import IntegerField, PasswordField
from wtforms.widgets import HiddenInput

strip_filter = lambda x: x.strip() if x else None

class BlogCreateForm(Form):
    title = StringField('Title', [validators.Length(min=1, max=255)],
                        filters=[strip_filter])
    body = TextAreaField('Contents', [validators.Length(min=1)],
                         filters=[strip_filter])

class BlogUpdateForm(BlogCreateForm):
    id = IntegerField(widget=HiddenInput())


class RegistrationForm(Form):
    username = StringField('Username', [validators.Length(min=1, max=255)],
                           filters=[strip_filter])
    password = PasswordField('Password', [validators.Length(min=3)])

__init__.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy


def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    authentication_policy = AuthTktAuthenticationPolicy('somesecret')
    authorization_policy = ACLAuthorizationPolicy()
    with Configurator(settings=settings,
                      authentication_policy=authentication_policy,
                      authorization_policy=authorization_policy) as config:
        config.include('.models')
        config.include('pyramid_jinja2')
        config.include('.routes')
        config.scan()
    return config.make_wsgi_app()

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
48
{% 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>
        <a href="{{request.route_url('register')}}">Register here</a>
    {% 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 %}

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
31
32
33
34
35
36
37
38
39
40
41
42
43
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
from ..forms import RegistrationForm
from ..models.user import User


@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)


@view_config(route_name='register',
             renderer='pyramid_blogr:templates/register.jinja2')
def register(request):
    form = RegistrationForm(request.POST)
    if request.method == 'POST' and form.validate():
        new_user = User(name=form.username.data)
        new_user.set_password(form.password.data.encode('utf8'))
        request.dbsession.add(new_user)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form}

models/user.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
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
)

from passlib.apps import custom_app_context as blogger_pwd_context


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):
        # is it cleartext?
        if password == self.password:
            self.set_password(password)

        return blogger_pwd_context.verify(password, self.password)

    def set_password(self, password):
        password_hash = blogger_pwd_context.encrypt(password)
        self.password = password_hash

Next: Summary.