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)
|
41 42 43 | 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 27 | 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()
config = Configurator(settings=settings,
authentication_policy=authentication_policy,
authorization_policy=authorization_policy)
config.include('pyramid_jinja2')
config.include('.models')
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 31 | 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.