Authentication

The Auth class provides a means of authenticating users of the site. It is designed to work out-of-the-box with a simple User model, but can be heavily customized.

The Auth system is comprised of a single class which is responsible for coordinating incoming requests to your project with known users. It provides the following:

  • views for login and logout

  • model to store user data (or you can provide your own)

  • mechanism for identifying users across requests (uses session storage)

All of these pieces can be customized, but the default out-of-box implementation aims to provide a good starting place.

The auth system is also designed to work closely with the Admin Interface.

Getting started

To provide a method for users to authenticate with your site, instantiate an Auth backend for your project:

from flask import Flask

from flask_peewee.auth import Auth
from flask_peewee.db import Database

app = Flask(__name__)
db = Database(app)

# needed for authentication
auth = Auth(app, db)

Note

user is a reserved word in Postgres. Pass db_table to Auth to override the table name.

Marking areas of the site as login required

If you want to mark specific areas of your site as requiring auth, you can decorate views using the Auth.login_required() decorator:

@app.route('/private/')
@auth.login_required
def private_timeline():
    user = auth.get_logged_in_user()

    # ... display the private timeline for the logged-in user

If the request comes from someone who has not logged-in with the site, they are redirected to the Auth.login() view, which allows the user to authenticate. After successfully logging-in, they will be redirected to the page they requested initially.

Requiring specific permissions

Auth.login_required() only checks that someone is logged in. When a view should be restricted further, two more decorators are available:

  • Auth.admin_required() additionally requires the user’s admin flag to be set.

  • Auth.test_user() takes a predicate fn(user) and builds a decorator that requires a logged-in user for whom it returns a truthy value.

In fact login_required and admin_required are nothing more than test_user(lambda user: True) and test_user(lambda user: user.admin). Use test_user to express any rule you like:

@app.route('/staff/')
@auth.test_user(lambda user: user.is_staff)
def staff_area():
    # only reachable by a logged-in user whose is_staff attribute is truthy
    ...

A request that fails the check is redirected to the login view, exactly like login_required.

Retrieving the current user

Whenever in a request context, the currently logged-in user is available by calling Auth.get_logged_in_user(), which will return None if the requesting user is not logged in.

The auth system also registers a pre-request hook that stores the currently logged-in user in the special flask variable g.

Logging users in and out programmatically

Sometimes you need to establish the session yourself, for instance to log a user in immediately after they register. Auth.login_user() and Auth.logout_user() do exactly that from within a request:

user = User(username='huey', email='huey@example.com', active=True)
user.set_password('meow')
user.save()

auth.login_user(user)   # huey is now logged in for subsequent requests

logout_user() ends the session. By default it removes only flask-peewee’s own session keys, leaving any other data you’ve stored in the session intact. Pass clear_session=True when constructing Auth to have logout wipe the entire session too:

auth = Auth(app, db, clear_session=True)

login_user marks the session permanent, so PERMANENT_SESSION_LIFETIME bounds how long a login lasts (flask defaults it to 31 days). The auth views flash their outcomes in the success and danger categories, picked up by any base template that renders flashed messages.

Revocable sessions

A login normally lives in the cookie alone, so nothing on the server can end it early. Pass session_field naming a CharField on the user model and each login also carries a token stored on that row:

class User(db.Model, BaseUser):
    ...
    session_token = CharField(default='')

auth = Auth(app, db, user_model=User, session_field='session_token')

The token is generated on the user’s first login and reused after, so all of that user’s sessions share it. A request counts as logged in only while the token in its cookie matches the row. Clearing the field ends every session for the user, which is what logout_user() now does, and what Auth.revoke_session_token() does from outside a request:

auth.revoke_session_token(user)   # logged out everywhere

With the default user model the field is added for you.

Adding registration

The auth views cover login and logout. A signup view is a few lines on top of Auth.login_user():

@app.route('/signup/', methods=['GET', 'POST'])
def signup():
    if request.method == 'POST':
        user = auth.User(username=request.form['username'],
                         email=request.form['email'],
                         active=True)
        user.set_password(request.form['password'])
        user.save()
        auth.login_user(user)
        return redirect(url_for('private_timeline'))
    return render_template('signup.html')

A real signup view also validates the input and handles duplicate usernames. The example app’s /join/ view shows the uniqueness check.

Password reset

Pass reset=True to add two more views: /forgot/ accepts an email address and sends a signed reset link, and /reset/<token>/ lets the holder of a valid link choose a new password. Delivering the email is the application’s job, so enabling the flag means overriding Auth.send_reset_email():

class MyAuth(Auth):
    def send_reset_email(self, user, reset_url):
        mailer.send(user.email, 'Password reset', reset_url)

auth = MyAuth(app, db, reset=True)

Tokens are signed with the app’s SECRET_KEY and expire after Auth.reset_token_max_age seconds (default 3600). Each token is also bound to the user’s current password hash, so completing a reset (or any other password change) invalidates outstanding tokens. The forgot view flashes the same message whether or not the email matched an account.

Accessing the user in the templates

The auth system registers a template context processor which makes the logged-in user available in any template:

{% if user %}
  <p>Hello {{ user.username }}</p>
{% else %}
  <p>Please <a href="{{ url_for('auth.login') }}?next={{ request.path }}">log in</a></p>
{% endif %}

Using a custom “User” model

It is easy to use your own model for the User, though depending on the amount of changes it may be necessary to override methods in both the Auth and Admin classes.

Unless you want to override the default behavior of the Auth class’ mechanism for actually authenticating users (which you may want to do if relying on a 3rd-party for auth), be sure your User model implements two methods:

  • set_password(password): takes a raw password and stores an encrypted version on model

  • check_password(password): returns whether or not the supplied password matches the one stored on the model instance

The default authenticate and get_logged_in_user queries also expect username, password and active fields on the model.

Note

The BaseUser mixin provides default implementations of these two methods.

Here’s a simple example of extending the auth system to use a custom user model:

from flask_peewee.auth import BaseUser # <-- implements set_password and check_password

app = Flask(__name__)
db = Database(app)

# create our custom user model, mixing in BaseUser for the default
# "set_password" and "check_password" implementations
class User(db.Model, BaseUser):
    username = CharField()
    password = CharField()
    email = CharField()
    active = BooleanField(default=True)

    # ... our custom fields ...
    is_superuser = BooleanField(default=False)


# create a modeladmin for it
class UserAdmin(ModelAdmin):
    columns = ('username', 'email', 'is_superuser',)

    # Hash a changed password before the save, so the raw password is
    # never written to the database.
    def save_model(self, instance, form, adding=False):
        orig_password = instance.password
        form.populate_obj(instance)

        if form.password.data != orig_password:
            instance.set_password(form.password.data)

        instance.save(force_insert=adding)
        return instance


# subclass Auth so we can return our custom classes
class CustomAuth(Auth):
    def get_user_model(self):
        return User

    def get_model_admin(self, model_admin=None):
        return UserAdmin

# instantiate the auth
auth = CustomAuth(app, db)

Here’s how you might integrate the custom auth with the admin area of your site:

# subclass Admin to check for whether the user is a superuser
class CustomAdmin(Admin):
    def check_user_permission(self, user):
        return user.is_superuser

# instantiate the admin
admin = CustomAdmin(app, auth)

admin.register(User, UserAdmin)
admin.setup()