Quickstart

Requirements

  • Python 3.6+

Note on Requirements

I do not currently plan to support older versions of python. Python 2 support is very unlikely to arrive as the original author is a die-hard believer in python 3. As for older versions of python 3, my test harnesses depend on some features only available in python 3.4 and up.

Installation

Note

flask-praetorian does not support distutils or setuptools because the author has very strong feelings about python packaging and the role pip plays in taking us into a bright new future of standardized and usable python packaging

Install from pypi

This will install the latest release of flask-praetorian from pypi via pip:

$ pip install flask-praetorian

Install latest version from github

If you would like a version other than the latest published on pypi, you may do so by cloning the git repository:

$ git clone https://github.com/dusktreader/flask-praetorian.git

Next, checkout the branch or tag that you wish to use:

$ cd flask-praetorian
$ git checkout integration

Finally, use poetry to install from the local directory:

$ poetry install

Example

A minimal example of how to use the flask-praetorian decorators is included:

import flask
import tempfile
import flask_sqlalchemy
import flask_praetorian
import flask_cors

db = flask_sqlalchemy.SQLAlchemy()
guard = flask_praetorian.Praetorian()
cors = flask_cors.CORS()


# A generic user model that might be used by an app powered by flask-praetorian
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.Text, unique=True)
    hashed_password = db.Column(db.Text)
    roles = db.Column(db.Text)
    is_active = db.Column(db.Boolean, default=True, server_default="true")

    @property
    def identity(self):
        """
        *Required Attribute or Property*

        flask-praetorian requires that the user class has an ``identity`` instance
        attribute or property that provides the unique id of the user instance
        """
        return self.id

    @property
    def rolenames(self):
        """
        *Required Attribute or Property*

        flask-praetorian requires that the user class has a ``rolenames`` instance
        attribute or property that provides a list of strings that describe the roles
        attached to the user instance
        """
        try:
            return self.roles.split(",")
        except Exception:
            return []

    @property
    def password(self):
        """
        *Required Attribute or Property*

        flask-praetorian requires that the user class has a ``password`` instance
        attribute or property that provides the hashed password assigned to the user
        instance
        """
        return self.hashed_password

    @classmethod
    def lookup(cls, username):
        """
        *Required Method*

        flask-praetorian requires that the user class implements a ``lookup()``
        class method that takes a single ``username`` argument and returns a user
        instance if there is one that matches or ``None`` if there is not.
        """
        return cls.query.filter_by(username=username).one_or_none()

    @classmethod
    def identify(cls, id):
        """
        *Required Method*

        flask-praetorian requires that the user class implements an ``identify()``
        class method that takes a single ``id`` argument and returns user instance if
        there is one that matches or ``None`` if there is not.
        """
        return cls.query.get(id)

    def is_valid(self):
        return self.is_active


# Initialize flask app for the example
app = flask.Flask(__name__)
app.debug = True
app.config["SECRET_KEY"] = "top secret"
app.config["JWT_ACCESS_LIFESPAN"] = {"hours": 24}
app.config["JWT_REFRESH_LIFESPAN"] = {"days": 30}

# Initialize the flask-praetorian instance for the app
guard.init_app(app, User)

# Initialize a local database for the example
local_database = tempfile.NamedTemporaryFile(prefix="local", suffix=".db")
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///{}".format(local_database)
db.init_app(app)

# Initializes CORS so that the api_tool can talk to the example app
cors.init_app(app)

# Add users for the example
with app.app_context():
    db.create_all()
    db.session.add(
        User(
            username="TheDude",
            hashed_password=guard.hash_password("abides"),
        )
    )
    db.session.add(
        User(
            username="Walter",
            hashed_password=guard.hash_password("calmerthanyouare"),
            roles="admin",
        )
    )
    db.session.add(
        User(
            username="Donnie",
            hashed_password=guard.hash_password("iamthewalrus"),
            roles="operator",
        )
    )
    db.session.add(
        User(
            username="Maude",
            hashed_password=guard.hash_password("andthorough"),
            roles="operator,admin",
        )
    )
    db.session.commit()


# Set up some routes for the example


@app.route("/login", methods=["POST"])
def login():
    """
    Logs a user in by parsing a POST request containing user credentials and
    issuing a JWT token.
    .. example::
       $ curl http://localhost:5000/login -X POST \
         -d '{"username":"Walter","password":"calmerthanyouare"}'
    """
    req = flask.request.get_json(force=True)
    username = req.get("username", None)
    password = req.get("password", None)
    user = guard.authenticate(username, password)
    ret = {"access_token": guard.encode_jwt_token(user)}
    return (flask.jsonify(ret), 200)


@app.route("/protected")
@flask_praetorian.auth_required
def protected():
    """
    A protected endpoint. The auth_required decorator will require a header
    containing a valid JWT
    .. example::
       $ curl http://localhost:5000/protected -X GET \
         -H "Authorization: Bearer <your_token>"
    """
    return flask.jsonify(
        message="protected endpoint (allowed user {})".format(
            flask_praetorian.current_user().username,
        )
    )


@app.route("/protected_admin_required")
@flask_praetorian.roles_required("admin")
def protected_admin_required():
    """
    A protected endpoint that requires a role. The roles_required decorator
    will require that the supplied JWT includes the required roles
    .. example::
       $ curl http://localhost:5000/protected_admin_required -X GET \
          -H "Authorization: Bearer <your_token>"
    """
    return flask.jsonify(
        message="protected_admin_required endpoint (allowed user {})".format(
            flask_praetorian.current_user().username,
        )
    )


@app.route("/protected_operator_accepted")
@flask_praetorian.roles_accepted("operator", "admin")
def protected_operator_accepted():
    """
    A protected endpoint that accepts any of the listed roles. The
    roles_accepted decorator will require that the supplied JWT includes at
    least one of the accepted roles
    .. example::
       $ curl http://localhost/protected_operator_accepted -X GET \
         -H "Authorization: Bearer <your_token>"
    """
    return flask.jsonify(
        message="protected_operator_accepted endpoint (allowed usr {})".format(
            flask_praetorian.current_user().username,
        )
    )


# Run the example
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

The above code can be found in example/basic.py.