User authentication with webapp2 on Google App Engine
The webapp2 framework on Google App Engine for Python 2.7 is definitely a step forward from the original webapp.
Despite the increase in flexibility and functionality, however, there are a few items that are still more laborious than in other frameworks. The most notable aspect is user account management.
Unsurprisingly, since it is meant to run on Google’s App Engine, using Google Accounts with webapp2 takes one line of code. OpenID authentication, while still defined experimental, is almost trivial to implement as well. There are some open source projects like SimpleAuth that attempt to offer a standard and unified API to handle signing in with Google, OAuth and OpenID accounts.
While it generally makes sense to offer support for authentication through popular services – it decreases friction for new users to try a service – in some cases users may prefer having a special login to access your application.
As experience teaches us, managing passwords securely is not a trivial task, and users legitimately expect application developers to take all the necessary measures to protect their passwords.
Since this is a use case that has to be considered countless time, there is significant value in using library functions to handle user accounts.
Here is how to do that using the functionalities embedded in the webapp2_extras
package that is distributed with all standard installations of App Engine for Python 2.7.
Table of Contents
Basics and prerequisites
Our main interface when dealing with authentication is
webapp2_extras.auth
. This module leverages the rest of webapp2’s infrastructure to offer us a simpler way to manage user authentication.
In particular, it relies on
webapp2_extras.security
to handle password hashing (so that passwords are never stored in clear text) and random string generation;webapp2_extras.sessions
to identify requests coming from the same user as part of a client-server conversation.
Since we are implementing our own user database, we will need to define a custom model class to represent users in our application. The auth framework described here works under the assumption that this model class defines the following instance method:
get_id(self)
and the following class methods:
get_by_auth_token(cls, user_id, token)
get_by_auth_password(cls, auth_id, password)
create_auth_token(cls, user_id)
delete_auth_token(cls, user_id, token)
The specific role of each of those methods is described here.
In addition to that, if you are interested in using the code for password reset provided as part of this tutorial, you will also need to implement the following class method:
get_by_auth_token(cls, user_id, token, subject='auth')
For each request, our application will then be able to do the following:
- read a cookie to figure out whether the current belongs to an existing session,
- if a cookie is found, read an authentication from it and load the corresponding session (if present) from the session store backend,
- loads some cached user information from the session.
We will be able to tune some details of the process via configuration options.
Extending the default User model
webapp2 already contains a reference User model for Google App Engine that uses NDB for storage. If you are willing to use that, you will find it good enough for most of the needs you may have.
In particular,
- it is an Expando model – it can include properties that were not specified as part of the class definition but are added at run-time, so it will not be a problem if your application needs to store some specific user information. New properties are indexed by default, so queries should still be fast.
- it allows you to set uniqueness constraints – while NDB does not support unique properties by default, webapp2’s User model uses a custom mechanism to allow to allow that.
Oddly enough – since the User class is clearly well designed – the reference implementation does not offer any method to update the password for a given user once it has been set. Because of that, it is a good idea to extend the User model to add a set_password
method that will have the responsibility to securely hash passwords using the security module.
You can find an example User model implementation below: it will contain the password setter and the get_by_auth_token
class method (modeled after the one already provided by webapp2) that has been introduced in the previous section.
import time
import webapp2_extras.appengine.auth.models
from google.appengine.ext import ndb
from webapp2_extras import security
class User(webapp2_extras.appengine.auth.models.User):
def set_password(self, raw_password):
"""Sets the password for the current user
:param raw_password:
The raw password which will be hashed and stored
"""
self.password = security.generate_password_hash(raw_password, length=12)
@classmethod
def get_by_auth_token(cls, user_id, token, subject='auth'):
"""Returns a user object based on a user ID and token.
:param user_id:
The user_id of the requesting user.
:param token:
The token string to be verified.
:returns:
A tuple ``(User, timestamp)``, with a user object and
the token timestamp, or ``(None, None)`` if both were not found.
"""
token_key = cls.token_model.get_key(user_id, subject, token)
user_key = ndb.Key(cls, user_id)
# Use get_multi() to save a RPC call.
valid_token, user = ndb.get_multi([token_key, user_key])
if valid_token and user:
timestamp = int(time.mktime(valid_token.created.timetuple()))
return user, timestamp
return None, None
Setting up the configuration
With what we have seen so far, we are now able to configure our application. We will need to set some properties as follows:
config = {
'webapp2_extras.auth': {
'user_model': 'models.User',
'user_attributes': ['name']
},
'webapp2_extras.sessions': {
'secret_key': 'YOUR_SECRET_KEY'
}
}
In particular,
user_model
is the name of the custom User model class we described earlier,user_attributes
is a list of attributes in the User model that will be cached in the session. Ideally, frequently accessed properties should be stored here. The full User model will be accessible by querying the datastore.secret_key
is the key used to secure the hash signature calculation for session cookies.
Creating a base handler class
Before writing the actual handlers that will implement the business logic to sign up and authenticate users we will group some utility functions in a base handler class, which will be extended by all the following handler classes.
This will ensure that all handlers will inherit a set of useful utility functions and properties to access user data and infrastructure classes, but also ensures that all session data is properly saved on each request (see dispatch
).
class BaseHandler(webapp2.RequestHandler):
@webapp2.cached_property
def auth(self):
"""Shortcut to access the auth instance as a property."""
return auth.get_auth()
@webapp2.cached_property
def user_info(self):
"""Shortcut to access a subset of the user attributes that are stored
in the session.
The list of attributes to store in the session is specified in
config['webapp2_extras.auth']['user_attributes'].
:returns
A dictionary with most user information
"""
return self.auth.get_user_by_session()
@webapp2.cached_property
def user(self):
"""Shortcut to access the current logged in user.
Unlike user_info, it fetches information from the persistence layer and
returns an instance of the underlying model.
:returns
The instance of the user model associated to the logged in user.
"""
u = self.user_info
return self.user_model.get_by_id(u['user_id']) if u else None
@webapp2.cached_property
def user_model(self):
"""Returns the implementation of the user model.
It is consistent with config['webapp2_extras.auth']['user_model'], if set.
"""
return self.auth.store.user_model
@webapp2.cached_property
def session(self):
"""Shortcut to access the current session."""
return self.session_store.get_session(backend="datastore")
def render_template(self, view_filename, params={}):
user = self.user_info
params['user'] = user
path = os.path.join(os.path.dirname(__file__), 'views', view_filename)
self.response.out.write(template.render(path, params))
def display_message(self, message):
"""Utility function to display a template with a simple message."""
params = {
'message': message
}
self.render_template('message.html', params)
# this is needed for webapp2 sessions to work
def dispatch(self):
# Get a session store for this request.
self.session_store = sessions.get_store(request=self.request)
try:
# Dispatch the request.
webapp2.RequestHandler.dispatch(self)
finally:
# Save all sessions.
self.session_store.save_sessions(self.response)
Registration: create new users
If we are using the model class discussed in the previous sections, in order to create a new User we just need to call the create_user
method.
class SignupHandler(BaseHandler):
def get(self):
self.render_template('signup.html')
def post(self):
user_name = self.request.get('username')
email = self.request.get('email')
name = self.request.get('name')
password = self.request.get('password')
last_name = self.request.get('lastname')
unique_properties = ['email_address']
user_data = self.user_model.create_user(user_name,
unique_properties,
email_address=email, name=name, password_raw=password,
last_name=last_name, verified=False)
if not user_data[0]: #user_data is a tuple
self.display_message('Unable to create user for email %s because of \
duplicate keys %s' % (user_name, user_data[1]))
return
user = user_data[1]
user_id = user.get_id()
token = self.user_model.create_signup_token(user_id)
verification_url = self.uri_for('verification', type='v', user_id=user_id,
signup_token=token, _full=True)
msg = 'Send an email to user in order to verify their address. \
They will be able to do so by visiting <a href="{url}">{url}</a>'
self.display_message(msg.format(url=verification_url))
create_user
will accept the following parameters:
- an authentication id: the token (such as a username or an email address) users will use to identify themselves when trying to access our application. While users can have multiple authentication ids, only one is allowed at creation.
- a list of unique properties (in this case,
email_address
): if this is specified, webapp2 will not allow us to create new users if others share the same values for the properties in this list. - name/value pairs that are going to be set as properties of the resulting User model. If you want to add your own fields, this where to do it. If a parameter called
password_raw
is present, it will be assumed to be the user password that will be used for authentication; webapp2 will hash it and store the hash the password field: we do not want to store passwords in clear text, do we?
We also create a signup token and associate it to the newly created account: we will use to confirm the email address that has been provided during registration.
Note that we are accessing the model as self.user_model
rather than calling it directly, so that we are free to change which implementation to use by updating the application configuration.
Login and logout
If users are created as in the previous session, login is quite simple: the get_user_by_password
method can be used to retrieve a user by their credentials. In addition to the user credentials, the method accepts some additional parameters. The one we care about (and the only one we use here) is remember
: when set to True
, the cookie used to identify the session is saved as persistent and the browser will keep it even after the user will close its window.
class LoginHandler(BaseHandler):
def get(self):
self._serve_page()
def post(self):
username = self.request.get('username')
password = self.request.get('password')
try:
u = self.auth.get_user_by_password(username, password, remember=True)
self.redirect(self.uri_for('home'))
except (InvalidAuthIdError, InvalidPasswordError) as e:
logging.info('Login failed for user %s because of %s', username, type(e))
self._serve_page(True)
def _serve_page(self, failed=False):
username = self.request.get('username')
params = {
'username': username,
'failed': failed
}
self.render_template('login.html', params)
The implementation above renders the login form when the request comes via GET and processes the credentials upon POST. When authentication fails it renders the login form and passes the username to the template so that the corresponding field can be pre-filled.
Implementing logout is even simpler: it is sufficient to get rid of the user session.
class LogoutHandler(BaseHandler):
def get(self):
self.auth.unset_session()
self.redirect(self.uri_for('home'))
Email confirmation and password reset
Signup tokens are one of the undocumented features of webapp2 (and they may possibly subject to change), but they can be quite handy when implementing a flow to confirming email addresses or recover passwords.
As mentioned before, the webapp2 uses authentication tokens to identify users after they logged in: they are meant to be securely shared by a client and the server and exchanged when a client needs to prove its identity.
As you probably imagine, this mechanism can be generalized to handle email confirmations and password resets: when websites send us an activation link after a registration, the URL usually contain their equivalent of signup tokens.
webapp2 sets a subject
property for each of the tokens it generates, so the only difference between auth token and signup token is the value for that property. So, why do we want to use signup tokens?
Setting a different value for that property allows us to partition tokens by their purpose: we can then implement useful features as deleting all the password reset tokens that have not been used in 48 hours.
Here is a sample verification handler that is able to process email verification links:
class VerificationHandler(BaseHandler):
def get(self, *args, **kwargs):
user = None
user_id = kwargs['user_id']
signup_token = kwargs['signup_token']
verification_type = kwargs['type']
# it should be something more concise like
# self.auth.get_user_by_token(user_id, signup_token
# unfortunately the auth interface does not (yet) allow to manipulate
# signup tokens concisely
user, ts = self.user_model.get_by_auth_token(int(user_id), signup_token,
'signup')
if not user:
logging.info('Could not find any user with id "%s" signup token "%s"',
user_id, signup_token)
self.abort(404)
# store user data in the session
self.auth.set_session(self.auth.store.user_to_dict(user), remember=True)
if verification_type == 'v':
# remove signup token, we don't want users to come back with an old link
self.user_model.delete_signup_token(user.get_id(), signup_token)
if not user.verified:
user.verified = True
user.put()
self.display_message('User email address has been verified.')
return
elif verification_type == 'p':
# supply user to the page
params = {
'user': user,
'token': signup_token
}
self.render_template('resetpassword.html', params)
else:
logging.info('verification type not supported')
self.abort(404)
This handler is meant to be used with a route using a template that matches URLs like /v/USERID-TOKEN
. You can configure it as follows (please refer to the sample code at the end of this article for the full routes configuration):
webapp2.Route('/<type:v|p>/<user_id:\d+>-<signup_token:.+>',
handler=VerificationHandler, name='verification')
Two minor notes on this item:
- For increased security, we may require users to enter their password before authenticating them.
- Ideally, we may want to use a different subject for email confirmation and password reset tokens.
Ensure users are logged in
Now that everything else is in place, we can decide whether users are allowed to access certain resources depending on their logged in state.
The following decorator can be used to annotate handler methods that require users to be logged in.
def user_required(handler):
"""
Decorator that checks if there's a user associated with the current session.
Will also fail if there's no session present.
"""
def check_login(self, *args, **kwargs):
auth = self.auth
if not auth.get_user_by_session():
self.redirect(self.uri_for('login'), abort=True)
else:
return handler(self, *args, **kwargs)
return check_login
Just placing @user_required
, as in the following example, before those methods will ensure that anonymous users will be directed to a login page when attempting to go through the annotated handler method.
class AuthenticatedHandler(BaseHandler):
@user_required
def get(self):
self.render_template('authenticated.html')
Finishing touches
Before actually using this in code in production, there is at least one task we should take care of: calls that send passwords (like login, signup, password reset) should be using https
.
This is quite easy to do and the documentation is quite straightforward: just follow the instructions here and you will be all set.
Your app.yaml file should include the following once you are done:
handlers:
- url: /signup
script: main.app
secure: always
- url: /login
script: main.app
secure: always
- url: /forgot
script: main.app
secure: always
- url: .*
script: main.app
libraries:
- name: webapp2
version: "2.5.1"
Of course, we will also need some views to be able to use this application. While this is the typical task that is left as an exercise to the reader, the example implementation you will find in the following section will contain a fully working application you can play with.
Reference code
You can find a ready to use application skeleton on GitHub. Feel free to play with it to experiment the full flow described in this post, use it to bootstrap your project and to improve on it. Just post a comment if you have any question or suggestion.
If you are interested in learning more on App Engine, you may want to check out Programming Google App Engine ( Kindle edition)