Customs - Authentication made easy

Passport.js inspired library for setting up server authentication in Python. Customs creates a protective layer around Flask APIs with minimal configuration and allows users to configure and use multiple authentication strategies with ease.

Concept

Customs consists of a single customs object that can use strategies to protect API endpoints, or create a safe_zone around a set of endpoints.

Batteries included

Customs comes out of the box with the following strategies:

  • Local

  • Basic

  • JWT

  • Google

  • Github

  • Facebook

The list is growing, but if you still cannot find what you’re looking for it is very easy to create a specific strategy for your purpose.

Quickstart

Getting started with Customs is easy, just install Customs, configure an authentication strategy and start protecting your endpoints. In this quickstart guid we’ll set up basic authentication to protect a specific route. Check out the rest of the docs and examples to see all possibilities.

1. Install Customs

Install the package using pip:

$ pip install customs

2. Configure a strategy

Pick a base strategy to work from, in this case the BasicStrategy. This strategy will automatically check the Authorization header in the request and look for base64 encoded credentials. All we got to do here is tell the strategy how to check the username and password and how to get user information from a username.

class BasicAuthentication(BasicStrategy):
    def validate_credentials(self, username: str, password: str) -> Dict:
        """ Method to validate credentials (username and password).
        """

        # If the user is in the database and the password is correct
        if username in DATABASE and DATABASE[username].get("password") == password:
            return DATABASE[username]

        # Otherwise raise an exception
        else:
            raise UnauthorizedException()

    def get_or_create_user(self, user: Dict) -> Dict:
        """ Method to get user information from the database (or optionally create the user in the database).
        """

        # If the user exists in our database, return the information
        if user.get("username") in DATABASE:
            return DATABASE[user["username"]]

        # If the username is not in the database, raise an exception
        else:
            raise UnauthorizedException()

As you can see we’ve created a subclass of the BasicStrategy and we’ve created methods that tell the strategy how to interact with the database. That’s it!

3. Protect your app

Now we’re ready to use the strategy to protect an application. To so do, we start with a Customs object which will protect our endpoints or creates a safe_zone for a set of endpoints. Lets stick with a single endpoint for now.

from flask import Flask
from customs import Customs

# Create the app, and the customs
app = Flask(__name__)
customs = Customs(app)

# ... The strategy definition we've created before goes here

# Make an instance of our basic authentication strategy
basic_authentication = BasicAuthentication()

@app.route("/")
def index():
    return "This is an open route, everyone has access"

@app.route("/protected")
@customs.protect(strategies=[basic_authentication])
def protected():
    return "This is a protected route, users only!"

This app now has 2 routes. One is open to the world (http://localhost:5000/) and one is protected with basic authentication (http://localhost:5000/protected).

For full examples head over to the examples/ directory.

Installation

Installation of Customs is very easy, just use pip:

$ pip install customs

API

This page contains generated documentation from the Customs code, describing the available classes and methods.

Customs

class customs.customs.Customs(*args, **kwargs)

Customs is a protective layer that makes sure every incoming request is properly authenticated and checked. Customs can define “safe zones”, like parts of your API, or protect individual routes. Customs is intended as middleware for Flask applications.

Parameters
  • app (Flask) – The Flask application to mount this middleware on

  • use_sessions (bool, optional) – Whether or not to use sessions for storing user information. Defaults to True.

  • session_timeout (timedelta, optional) – The default expiration time for sessions. Defaults to timedelta(days=31).

  • user_class (Type, optional) – The class to use for parsing user information. Defaults to dict.

  • unauthorized_redirect_url (str, optional) – The URL to redirect to when a user tries to access an endpoint without proper authorization

Examples

>>> from flask import Flask
>>> from customs import Customs
>>> app = Flask(__name__)
>>> customs = Customs(app)
protect(strategies: Union[List[Union[str, customs.strategies.base_strategy.BaseStrategy]], str, customs.strategies.base_strategy.BaseStrategy]) → Callable

Decorator method that protects a specific route, using a set of strategies.

Parameters

strategies (Union[List[Union[str, BaseStrategy]], str, BaseStrategy]) – The strategy or list of strategies to use for protection. Can be a list of strategy names, list of strategy objects or an individual strategy by name or as object.

Returns

The wrapped view function

Return type

Callable

Examples

>>> @app.route("/test")
... @customs.protect(strategies=["basic"])
... def test_route():
...     return "Success"
register_strategy(name: str, strategy: customs.strategies.base_strategy.BaseStrategy)customs.customs.Customs

Register a strategy without using it for every route. Makes the strategy available by its name. Most strategies auto-register, so this method should not be needed very often.

Parameters
  • name (str) – The name of the strategy

  • strategy (BaseStrategy) – The strategy (which should inherit from BaseStrategy)

Returns

Returns this instance of customs for chaining

Return type

Customs

safe_zone(zone: Union[flask.blueprints.Blueprint, flask.app.Flask], strategies: List[Union[str, customs.strategies.base_strategy.BaseStrategy]])

Create a zone/section of the app that is protected with specific strategies. The zone can be an entire Flask application, or a section of the app in the form of a Blueprint.

Parameters
  • zone (Union[Blueprint, Flask]) – The zone to protect

  • strategies (List[str]) – The names of the strategies to use

Returns

The (now protected) input zone

Return type

Union[Blueprint, Flask]

Examples

>>> from flask import Flask
>>> from customs import Customs
>>> app = Flask(__name__)
>>> customs = Customs(app)
>>> # Define routes here ...
>>> customs.safe_zone(app, strategies=["basic"])

Strategies

Strategies define how the customs should protect a resource (endpoint, blueprint, or app). Strategies can be combined to create the desired protection. Strategies should be subclassed to tell them about application specific element, for example how to read a user from the database.

class customs.strategies.BasicStrategy

Strategy that enables authorization using the “basic” authorization header.

Examples

>>> class BasicAuthentication(BasicStrategy):
...     def get_or_create_user(self, user: Dict) -> Dict:
...         if user.get("username") in DATABASE:
...             return DATABASE[user["username"]]
...         else:
...             raise UnauthorizedException()
...     def validate_credentials(self, username: str, password: str) -> Dict:
...         if username in DATABASE and DATABASE[username].get("password") == password:
...             return DATABASE[username]
...         else:
...             raise UnauthorizedException()
authenticate(request: Union[werkzeug.wrappers.request.Request, flask.wrappers.Request]) → Any

Method that will extract the basic authorization header from the request, and will then call the validate_credentials method with a username and password. The validate_credentials method should be implemented by the user. This method is called by Customs internally and is not intended for external use.

Parameters

request (Union[Request, FlaskRequest]) – The incoming request (usually a Flask request)

Raises

UnauthorizedException – Raised when the user is not authorized (invalid or missing credentials)

Returns

The user information

Return type

Dict

class customs.strategies.FacebookStrategy(client_id: str, client_secret: str, scopes: Optional[List[str]] = None, enable_insecure: bool = False, endpoint_prefix: Optional[str] = None)

Authentication using Facebook as an OAuth2 provider.

get_user_info() → Dict

Method to get user info for the logged in user.

Raises

UnauthorizedException – When the user is not authenticated

Returns

The user profile

Return type

Dict

class customs.strategies.GithubStrategy(client_id: str, client_secret: str, scopes: Optional[List[str]] = None, enable_insecure: bool = False, endpoint_prefix: Optional[str] = None)

Authentication using Github as an OAuth2 provider.

validate_token() → Dict

Method to validate a Github token with Github.

Raises

UnauthorizedException – When the user isn’t authenticated or token is not valid

Returns

The data from the token

Return type

Dict

class customs.strategies.GoogleStrategy(client_id: str, client_secret: str, scopes: Optional[List[str]] = None, enable_insecure: bool = False, endpoint_prefix: Optional[str] = None)

Authentication using Google as an OAuth2 provider.

validate_token() → Dict

Method to validate a Google token with Google.

Raises

UnauthorizedException – When the user isn’t authenticated or token is not valid

Returns

The data from the token

Return type

Dict

class customs.strategies.JWTStrategy(key: Optional[str] = None)

Authentication using JWT tokens.

authenticate(request: Union[werkzeug.wrappers.request.Request, flask.wrappers.Request]) → Any

Method that will extract the JWT authorization header from the request, and will then call the deserialize_user method with the decoded content of the token. The validate_credentials method should be implemented by the user. This method is called by Customs internally and is not intended for external use.

Parameters

request (Union[Request, FlaskRequest]) – The incoming request (usually a Flask request)

Raises

UnauthorizedException – Raised when the user is not authorized (invalid or missing credentials)

Returns

The user information

Return type

Dict

sign(user: Any) → str

Sign a new token for the user. Serialize the user info before signing.

Parameters

user (Any) – The user data to serialize and sign

Returns

The signed token

Return type

str

class customs.strategies.LocalStrategy

Authentication using request information (e.g. arguments) with username and password.

authenticate(request: Union[werkzeug.wrappers.request.Request, flask.wrappers.Request]) → Any

Method should return the user info

Development

Initialize GitHooks

Git hooks can be used to automate certain version control steps, e.g. run tests and style checks before committing. Git hooks for this repository can be found at ./.githooks, but have to be initialized manually on the development machine with this command:

chmod +x .githooks/prepare-commit-msg
git config core.hooksPath .githooks

License

MIT License

Copyright (c) 2021 Gijs Wobben

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.