Advanced Porting topics

Porting       • Basics       • Customizations       • Advanced      


Flask-Login

We recommend NOT upgrading Flask-Login from v0.2.x to v0.3+ unless you need to.

Flask-Login changed user.is_authenticated, user.is_anonymous and user.is_active from being methods in v0.2 to being properties in v0.3+.

This difference often requires changes in many places (in code and in template files):

user.is_authenticated()  -->  user.is_authenticated
user.is_anonymous()      -->  user.is_anonymous
user.is_active()         -->  user.is_active

TokenManager() changes

The v0.6 TokenManager could only encrypt a single integer ID with less than 16 digits.

The v1.0+ TokenManager can now encrypt a list of items. Each item can be an integer or a string. Integers can be of any size.

This change enables us to encrypt Mongo Object IDs as well as the last 8 bytes of a password, to invalidate tokens after their password changed.

As a result, the generated tokens are different, which will affect these areas:

  • v0.6 user-session tokens, that were stored in a browser cookie, are no longer valid in v1.0+ and the user will be required to login again.
  • Unused v0.6 password-reset tokens and user-invitation tokens, are no longer valid in v1.0+ and the affected users will have to issue new forgot-password emails and new user invitatin emails. This effect is mitigated by the fact that these tokens are meant to expire relatively quickly.
  • user-session tokens and password-reset tokens become invalid if the user changes their password.

Python getters and setters

If you are unable to change property names, you can use Python’s getters and setters to form a bridge between required property names and actual ones.

Here’s an example of how to map the v1.0 required email_confirmed_at property to your existing confirmed_at property:

# If the actual property (confirmed_at) name
# differs from required name (email_confirmed_at).
class User(db.Model, UserMixin)
        ...
    # Actual property
    confirmed_at = db.Column(db.DateTime())

    # Map required property name to actual property
    @property
    def email_confirmed_at(self):
        return self.confirmed_at

    @email_confirmed_at.setter
    def email_confirmed_at(self, value):
        self.confirmed_at = value

Supporting deprecated UserAuth data-models

Because the optional UserAuth and User have a one-to-one relationship to each other, you can use getters and setters to have Flask-User manage two objects while specifying only the one User class:

# This is your existing UserAuth data-model
# -----------------------------------------
class UserAuth(db.Model):
    id = db.Column(db.Integer, primary_key=True)

    # Relationship to user
    user_id = db.Column(db.Integer(), db.ForeignKey('users.id', ondelete='CASCADE'))
    user = db.relationship('User', uselist=False)

    # User authentication information
    username = db.Column(db.String(50), nullable=False, unique=True)
    password = db.Column(db.String(255), nullable=False, server_default='')
    email = db.Column(db.String(255), nullable=False, unique=True)
    confirmed_at = db.Column(db.DateTime())


# This is your existing User data-model
# -------------------------------------
class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)

    # User information
    active = db.Column('is_active', db.Boolean(), nullable=False, server_default='0')
    first_name = db.Column(db.String(100), nullable=False, server_default='')
    last_name = db.Column(db.String(100), nullable=False, server_default='')

    # One-to-one Relationship
    user_auth = db.relationship('UserAuth', uselist=False)

    # Create a UserAuth instance when a User instance is created
    def __init__(self, *args, **kwargs):
        super(User, self).__init__(*args, **kwargs)
        self.user_auth = UserAuth(user=self)


    # Map required v1.0 properties in the User data-model
    # to existing properties in the UserAuth data-model.
    # ---------------------------------------------------

    # Map the User.username field into the UserAuth.username field
    @property
    def username(self):
        return self.user_auth.username

    @username.setter
    def username(self, value)
        self.user_auth.username = value


    # Map the User.password field into the UserAuth.password field
    @property
    def password(self):
        return self.user_auth.password

    @password.setter
    def password(self, value)
        self.user_auth.password = value


    # Map the User.email field into the UserAuth.email field
    @property
    def email(self):
        return self.user_auth.email

    @email.setter
    def email(self, value)
        self.user_auth.email = value


    # Map the User.email_confirmed_at field into the UserAuth.confirmed_at field
    @property
    def email_confirmed_at(self):
        return self.user_auth.confirmed_at

    @email_confirmed_at.setter
    def email_confirmed_at(self, value)
        self.user_auth.confirmed_at = value


# Setup Flask-User
user_manager = UserManager(app, db, User)

# -----------------------------------------------------------------
# This code snippet has not yet been tested. You can email
# ling.thio@gmail.com when it works or when you encounter problems.
# When enough people tested this I will remove this comment.
# Thank you!

Logging in without confirmed email addresses

In v0.6, the USER_ENABLE_LOGIN_WITHOUT_CONFIRM_EMAIL=True setting allowed users to login regardless of whether their email addresses were confirmed or not. Sensitive views were protected with an @confirm_email_required view decorator. This left websites vulnerable to views that were unintentionally left unprotected.

In v1.0 we renamed the USER_ENABLE_LOGIN_WITHOUT_CONFIRM_EMAIL to USER_ALLOW_LOGIN_WITHOUT_CONFIRMED_EMAIL to better reflect what this setting does. and we deprecated the @confirm_email_required view decorator.

In v1.0, we reduced the opportunities for mistakes, by taking the opposite protection approach: Even with USER_ENABLE_LOGIN_WITHOUT_CONFIRM_EMAIL=True, views with @login_required, @roles_accepted, and @roles_required decorators continue to be protected against users without confirmed email addresses.

If you want unconfirmed users to access certain views, you will need to add the new @allow_unconfirmed_email decorator to each view that you choose to expose.

This decorator must follow the @route decorator (as usual)
THis decorator must precede any of the other Flask-Userdecorators
@route(...)
@allow_unconfirmed_email
@login_required
def unprotected_view():
    ...

Porting       • Basics       • Customizations       • Advanced