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.