User guide

Install

$ pip install falcon-auth2[jwt]
The above will install falcon-auth2 and also the dependencies to use the JWT authentication backend.
If you plan to use async falcon with ASGI run:
$ pip install falcon-auth2[jwt, async]

Usage

This package provides a falcon middleware to authenticate incoming requests using the selected authentication backend. The middleware allows excluding some routes or method from authentication. After a successful authentication the middleware adds the user identified by the request to the request.context. When using falcon v3+, the middleware also supports async execution.

See below WSGI example and ASGI example for complete examples.

import falcon
from falcon_auth2 import AuthMiddleware
from falcon_auth2.backends import BasicAuthBackend

def user_loader(attributes, user, password):
    if authenticate(user, password):
        return {"username": user}
    return None

auth_backend = BasicAuthBackend(user_loader)
auth_middleware = AuthMiddleware(auth_backend)
# use falcon.API in falcon 2
app = falcon.App(middleware=[auth_middleware])

class HelloResource:
    def on_get(self, req, resp):
        # req.context.auth is of the form:
        #
        #   {
        #       'backend': <instance of the backend that performed the authentication>,
        #       'user': <user object retrieved from the user_loader callable>,
        #       '<backend specific item>': <some extra data that may be added by the backend>,
        #       ...
        #   }
        user = req.context.auth["user"]
        resp.media = {"message": f"Hello {user['username']}"}

app.add_route('/hello', HelloResource())

Overriding authentication for a resource

The middleware allows each resource to customize the backend used for authentication or the excluded methods. A resource can also specify that does not need authentication.

from falcon_auth2 import HeaderGetter
from falcon_auth2.backends import GenericAuthBackend

class OtherResource:
    auth = {
        "backend": GenericAuthBackend(
            user_loader=lambda attr, user_header: user_header, getter=HeaderGetter("User")
        ),
        "exempt_methods": ["GET"],
    }

    def on_get(self, req, resp):
        resp.media = {"type": "No authentication for GET"}

    def on_post(self, req, resp):
        resp.media = {"info": f"User header {req.context.auth['user']}"}

app.add_route("/other", OtherResource())

class NoAuthResource:
    auth = {"auth_disabled": True}

    def on_get(self, req, resp):
        resp.media = "No auth in this resource"

    def on_post(self, req, resp):
        resp.media = "No auth in this resource"

app.add_route("/no-auth", NoAuthResource())

Examples

WSGI example

# flake8: noqa
import random

import falcon

from falcon_auth2 import AuthMiddleware
from falcon_auth2 import HeaderGetter
from falcon_auth2.backends import BasicAuthBackend
from falcon_auth2.backends import GenericAuthBackend
from falcon_auth2.backends import JWTAuthBackend
from falcon_auth2.backends import MultiAuthBackend

# To run this application with waitress (or any other wsgi server)
# waitress-serve --port 8080 readme_example:app
# You can then use httpie to interact with it. Example
# http :8080/hello -a foo:bar
# http :8080/hello 'Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiYXIiLCJpc3MiOiJhbiBpc3N1ZXIiLCJhdWQiOiJhbiBhdWQiLCJpYXQiOjE2MDk0NTkyMDAsIm5iZiI6MTYwOTQ1OTIwMCwiZXhwIjoxOTI0OTkyMDAwfQ.FDpE_-jL-reHrheIYVGbwdVf8g1HWFAoGJet_6zC2Tk'
# http :8080/no-auth
# http POST :8080/generic User:foo


def authenticate(user, password):
    # Check if the user exists and the password match.
    # This is just for the example
    return random.choice((True, False))


def basic_user_loader(attributes, user, password):
    if authenticate(user, password):
        return {"username": user, "kind": "basic"}
    return None


def jwt_user_loader(attributes, payload):
    # Perform additional authentication using the payload.
    # This is just an example
    if "sub" in payload:
        return {"username": payload["sub"], "kind": "jwt"}
    return None


# NOTE: this is just an example. A key should be propertly generated, like using:
# key=secrets.token_bytes(256)
key = "not-a-secret-key"
basic_backend = BasicAuthBackend(basic_user_loader)
jwt_backend = JWTAuthBackend(jwt_user_loader, key)
auth_backend = MultiAuthBackend((basic_backend, jwt_backend))
auth_middleware = AuthMiddleware(auth_backend)
# use falcon.API in falcon 2
app = falcon.App(middleware=[auth_middleware])


class HelloResource:
    def on_get(self, req, resp):
        # req.context.auth is of the form:
        #
        #   {
        #       'backend': <instance of the backend that performed the authentication>,
        #       'user': <user object retrieved from the user_loader callable>,
        #       '<backend specific item>': <some extra data that may be added by the backend>,
        #       ...
        #   }
        user = req.context.auth["user"]
        resp.media = {"message": f"Hello {user['username']} from {user['kind']}"}


app.add_route("/hello", HelloResource())


def user_header_loader(attr, user_header):
    # authenticate the user with the user_header
    return user_header


class GenericResource:
    auth = {
        "backend": GenericAuthBackend(user_header_loader, getter=HeaderGetter("User")),
        "exempt_methods": ["GET"],
    }

    def on_get(self, req, resp):
        resp.media = {"type": "No authentication for GET"}

    def on_post(self, req, resp):
        resp.media = {"info": f"User header {req.context.auth['user']}"}


app.add_route("/generic", GenericResource())


class NoAuthResource:
    auth = {"auth_disabled": True}

    def on_get(self, req, resp):
        resp.text = "No auth in this resource"

    def on_post(self, req, resp):
        resp.text = "No auth in this resource"


app.add_route("/no-auth", NoAuthResource())


def make_token():
    # the token above was generated by calling this example function
    from authlib.jose import jwt

    payload = {
        "sub": "bar",
        "iss": "an issuer",
        "aud": "an aud",
        "iat": 1609459200,
        "nbf": 1609459200,
        "exp": 1924992000,
    }
    print(jwt.encode({"alg": "HS256"}, payload, key))

ASGI example

# flake8: noqa
import random

import falcon.asgi

from falcon_auth2 import AuthMiddleware
from falcon_auth2 import HeaderGetter
from falcon_auth2.backends import BasicAuthBackend
from falcon_auth2.backends import GenericAuthBackend
from falcon_auth2.backends import JWTAuthBackend
from falcon_auth2.backends import MultiAuthBackend

# To run this application with uvicorn (or any other asgi server)
# uvicorn --port 8080 readme_example_async:app
# You can then use httpie to interact with it. Example
# http :8080/hello -a foo:bar
# http :8080/hello 'Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiYXIiLCJpc3MiOiJhbiBpc3N1ZXIiLCJhdWQiOiJhbiBhdWQiLCJpYXQiOjE2MDk0NTkyMDAsIm5iZiI6MTYwOTQ1OTIwMCwiZXhwIjoxOTI0OTkyMDAwfQ.FDpE_-jL-reHrheIYVGbwdVf8g1HWFAoGJet_6zC2Tk'
# http :8080/no-auth
# http POST :8080/generic User:foo


async def authenticate(user, password):
    # Check if the user exists and the password match.
    # This is just for the example
    return random.choice((True, False))


async def basic_user_loader(attributes, user, password):
    if await authenticate(user, password):
        return {"username": user, "kind": "basic"}
    return None


async def jwt_user_loader(attributes, payload):
    # Perform additional authentication using the payload.
    # This is just an example
    if "sub" in payload:
        return {"username": payload["sub"], "kind": "jwt"}
    return None


# NOTE: this is just an example. A key should be propertly generated, like using:
# key=secrets.token_bytes(256)
key = "not-a-secret-key"
basic_backend = BasicAuthBackend(basic_user_loader)
jwt_backend = JWTAuthBackend(jwt_user_loader, key)
auth_backend = MultiAuthBackend((basic_backend, jwt_backend))
auth_middleware = AuthMiddleware(auth_backend)
app = falcon.asgi.App(middleware=[auth_middleware])


class HelloResource:
    async def on_get(self, req, resp):
        # req.context.auth is of the form:
        #
        #   {
        #       'backend': <instance of the backend that performed the authentication>,
        #       'user': <user object retrieved from the user_loader callable>,
        #       '<backend specific item>': <some extra data that may be added by the backend>,
        #       ...
        #   }
        user = req.context.auth["user"]
        resp.media = {"message": f"Hello {user['username']} from {user['kind']}"}


app.add_route("/hello", HelloResource())


async def user_header_loader(attr, user_header):
    # authenticate the user with the user_header
    return user_header


class GenericResource:
    auth = {
        "backend": GenericAuthBackend(user_header_loader, getter=HeaderGetter("User")),
        "exempt_methods": ["GET"],
    }

    async def on_get(self, req, resp):
        resp.media = {"type": "No authentication for GET"}

    async def on_post(self, req, resp):
        resp.media = {"info": f"User header {req.context.auth['user']}"}


app.add_route("/generic", GenericResource())


class NoAuthResource:
    auth = {"auth_disabled": True}

    async def on_get(self, req, resp):
        resp.text = "No auth in this resource"

    async def on_post(self, req, resp):
        resp.text = "No auth in this resource"


app.add_route("/no-auth", NoAuthResource())


def make_token():
    # the token above was generated by calling this example function
    from authlib.jose import jwt

    payload = {
        "sub": "bar",
        "iss": "an issuer",
        "aud": "an aud",
        "iat": 1609459200,
        "nbf": 1609459200,
        "exp": 1924992000,
    }
    print(jwt.encode({"alg": "HS256"}, payload, key))