Skip to main content

𐌅𐌋𐌀𐌔𐌊-ᕓꝊ𐌵𐌂𐋅

Bot-challenge middleware for Flask. Intercepts unrecognized visitors, issues proof-of-work or CAPTCHA challenges, and grants HMAC-signed JWT access cookies to solvers.

PyPI Python License Issues Stars Downloads

from flask import Flask
from flask_vouch import Vouch

app = Flask(__name__)
vouch = Vouch(app, secret="change-me")

Bots get a browser challenge page. Humans solve it once, get a cookie, browse freely.

Install

pip install flask-Vouch

Optional extras:

pip install flask-Vouch[image]      # image-based captchas (Pillow, numpy)
pip install flask-Vouch[audio]      # audio captcha (numpy, scipy)

How it works

  1. Every suspicious unauthenticated request matching the configured rules is redirected to a challenge page.
  2. A proof-of-work challenge (SHA-256 Balloon by default) is issued.
  3. The browser solves it in JavaScript and POSTs to /.tollbooth/verify.
  4. A valid solution sets a signed JWT cookie subsequent requests pass through.

Quick start

from flask import Flask
from flask_vouch import Vouch

app = Flask(__name__)
vouch = Vouch(app, secret="change-me")

@app.route("/")
def index():
    return "You passed the challenge!"

@app.route("/internal")
@vouch.exempt
def internal():
    return "ok"

Application factory:

vouch = Vouch(secret="change-me")

def create_app():
    app = Flask(__name__)
    vouch.init_app(app)
    return app

SECRET_KEY fallback if no secret= is passed, app.config["SECRET_KEY"] is used automatically:

app.config["SECRET_KEY"] = "change-me"
vouch = Vouch()
vouch.init_app(app)

Configuration

Pass as kwargs or via app.config with the VOUCH_ prefix:

Parameter Default Description
secret SECRET_KEY HMAC/JWT signing key
policy default rules Policy instance
exclude [] Path regexes to skip entirely
json_mode False Return JSON challenge instead of HTML
cookie_name _tollbooth Access cookie name
cookie_ttl 604800 Cookie lifetime in seconds (7 days)
cookie_secure True Secure flag; only set over HTTPS
verify_path /.tollbooth/verify Challenge verification endpoint
challenge_handler SHA256Balloon Challenge implementation
blocklist None NetSet instance or list of them
app.config["VOUCH_COOKIE_NAME"] = "_v"
app.config["VOUCH_COOKIE_TTL"] = 3600

Route decorators

Decorator Behavior
@vouch.exempt Skip challenge entirely for this route
@vouch.protect Always run challenge check (overrides global allow)
@vouch.challenge Always issue a challenge regardless of policy
@vouch.block Deny detected crawlers outright; challenge or pass others

exempt also takes an endpoint name, vouch.exempt("static") for static files, vouch.exempt("admin.static") for a blueprint's.

Custom rules

from flask_vouch import Vouch, Policy, Rule

policy = Policy(
    rules=[
        Rule(name="allow-google", action="allow", user_agent="Googlebot"),
        Rule(name="block-scrapers", action="deny", user_agent="AhrefsBot|SemrushBot"),
        Rule(name="challenge-curl", action="challenge", difficulty=8, user_agent="curl"),
    ]
)

vouch = Vouch(app, secret="s", policy=policy)

Load the built-in ruleset from rules.json:

from flask_vouch import load_policy

vouch = Vouch(app, secret="s", policy=load_policy())

Rule fields:

Field Type Description
name str Identifier
action str allow · deny · challenge · weigh
user_agent str (regex) Match on User-Agent header
path str (regex) Match on request path
headers dict Match on arbitrary headers (regex values)
remote_addresses list[str] CIDR ranges to match
difficulty int Challenge difficulty (default: policy)
weight int Score added when action=weigh
blocklist bool Match IPs in the loaded netset
bogon_ip bool Match non-global / bogon IPs
crawler bool Match detected crawler user agents

Challenge types

from flask_vouch import (
    SHA256Balloon,              # default, proof of work    (SHA-256 balloon hashing)
    SHA256,                     # lightweight SHA-256 PoW
    ChainCaptcha,               # no-interaction iterated-SHA-256 PoW
    CharacterCaptcha,           # text CAPTCHA
    ImageCaptcha,               # image CAPTCHA             (requires [image])
    RotationCaptcha,            # rotation CAPTCHA          (requires [image])
    CupCaptcha,                 # cup fill CAPTCHA          (requires [image])
    SlidingCaptcha,             # sliding puzzle            (requires [image])
    CircleCaptcha,              # circle select CAPTCHA     (requires [image])
    TraceCaptcha,               # curve-trace CAPTCHA       (pure Python, kinematics)
    ImageGridCaptcha,           # image grid CAPTCHA        (requires [image])
    AudioCaptcha,               # audio CAPTCHA             (requires [audio])
    NavigatorAttestation,       # browser signal attestation
    QuirkProbe,                 # browser-engine quirk verification
    ThirdPartyCaptchaChallenge, # embed external CAPTCHAs
)

vouch = Vouch(app, secret="s", challenge_handler=CharacterCaptcha())

Custom handlers

Subclass ChallengeHandler, only challenge_type, verify and render_payload are required. to_difficulty applies the type's offset from DIFFICULTY_OFFSETS and template reads challenges/templates/<challenge-type>.html; override either one to change that.

from flask_vouch.challenges import ChallengeHandler, ChallengeType

class MyChallenge(ChallengeHandler):
    @property
    def challenge_type(self):
        return ChallengeType.SHA256

    def verify(self, random_data, nonce, difficulty):
        return str(nonce) == random_data[:4]

    def render_payload(self, challenge, verify_path, redirect):
        return {"id": challenge.id, "verifyPath": verify_path, "redirect": redirect}

CAPTCHAs that must hand the browser a page while keeping the answer server-side subclass SignedTokenHandler: issue_token(solution) returns the encrypted, HMAC-signed, expiring string stored as the challenge's random_data, and read_token(token) returns the solution back (raising once token_ttl passes). Handlers that render media additionally keep it in a RenderCache between generate_random_data and render_payload.

IP netset

A netset is a newline-delimited list of IPs, CIDR ranges, or start-end ranges (# comments ignored) the FireHOL ipset/netset format. NetSet loads one from a file path or URL, merges overlapping ranges, and answers membership in O(log n).

from flask_vouch import Vouch, NetSet

ns = NetSet()        # defaults to bundled blocklist.netset URL
ns.load()
ns.start_updates()   # auto-refresh daily in a daemon thread

vouch = Vouch(app, secret="s", blocklist=ns)

Custom source(s) path or URL:

ns = NetSet("https://example.com/bad-ips.netset")
many = NetSet.from_sources(["a.netset", "b.netset"])

vouch = Vouch(app, secret="s", blocklist=[ns1, ns2])

Redis backend

For multi-process / multi-worker deployments:

import redis
from flask_vouch.redis import RedisEngine
from flask_vouch import Vouch

r = redis.Redis()
engine = RedisEngine(r, secret="s")
vouch = Vouch(app, engine=engine)

Extras

ErrorHandler

from flask_vouch.extras import ErrorHandler

eh = ErrorHandler(bouncer=vouch)
eh.init_flask(app)

RateLimiter

from flask_vouch.extras import RateLimiter

rl = RateLimiter(default="100/minute")
rl.exempt("static")
rl.init_flask(app)

@app.route("/login")
@rl.limit("5/minute")
def login(): ...

init_flask buckets per endpoint, so every asset on a page shares one static budget, exempt it unless you want asset-heavy browsing to hit 429.

ThirdPartyCaptcha

from flask_vouch.extras import ThirdPartyCaptcha

tpc = ThirdPartyCaptcha(turnstile_site_key="...", turnstile_secret="...")
tpc.init_flask(app)

@app.route("/submit", methods=["POST"])
def submit():
    if not tpc.is_turnstile_valid():
        abort(403)
    ...

Formatting

pip install black isort
isort . && black .
npx prtfm

License

Apache-2.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flask_vouch-1.2.7.tar.gz (7.7 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flask_vouch-1.2.7-py3-none-any.whl (7.9 MB view details)

Uploaded Python 3

File details

Details for the file flask_vouch-1.2.7.tar.gz.

File metadata

  • Download URL: flask_vouch-1.2.7.tar.gz
  • Upload date:
  • Size: 7.7 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for flask_vouch-1.2.7.tar.gz
Algorithm Hash digest
SHA256 8ea0aab9a634d6f17b41231a729af475ed5680c6d5de454f049bfbb6c0c4e586
MD5 c709a3e01cbea665f1560cfb9dc703bc
BLAKE2b-256 abae489b73dc3323ba0ce6980052c794a39757f77b9a57e09565dbbea5ced87a

See more details on using hashes here.

Provenance

The following attestation bundles were made for flask_vouch-1.2.7.tar.gz:

Publisher: publish.yml on tn3w/flask-Vouch

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file flask_vouch-1.2.7-py3-none-any.whl.

File metadata

  • Download URL: flask_vouch-1.2.7-py3-none-any.whl
  • Upload date:
  • Size: 7.9 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for flask_vouch-1.2.7-py3-none-any.whl
Algorithm Hash digest
SHA256 dd39296a58c1beee42df1106bccb9e9179aefeee7bbd6054cd4c19e552e32028
MD5 af06eefb62471f5de1420fde5305b835
BLAKE2b-256 d849ede1fda22ba94ea1da05b9bc65d9de0afff93652de595df656735676b62f

See more details on using hashes here.

Provenance

The following attestation bundles were made for flask_vouch-1.2.7-py3-none-any.whl:

Publisher: publish.yml on tn3w/flask-Vouch

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.4.0

2 files

1.3.0

2 files

1.2.8

2 files

This release

1.2.7 This release

2 files

1.2.6

2 files

1.2.5

2 files

1.2.4

2 files

1.2.3

2 files

1.2.2

2 files

1.2.1

2 files

1.2.0

2 files

1.1.1

2 files

1.1.0

2 files

1.0.4

2 files

1.0.3

2 files

1.0.2

2 files

1.0.1

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page