Bot-challenge Python middleware issuing brief challenges, granting solvers signed access cookies.
Project description
𐌅𐌋𐌀𐌔𐌊-ᕓꝊ𐌵𐌂𐋅
Bot-challenge middleware for Flask — intercepts unrecognized visitors, issues proof-of-work or CAPTCHA challenges, and grants HMAC-signed JWT access cookies to solvers.
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)
pip install flask-Vouch[rotation] # RotationCaptcha (Pillow, numpy)
pip install flask-Vouch[audio] # audio captcha (numpy, scipy)
How it works
- Every request without a valid signed cookie is intercepted.
- A proof-of-work challenge (SHA-256 Balloon by default) is issued.
- The browser solves it in JavaScript and POSTs to
/.tollbooth/verify. - 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) |
verify_path |
/.tollbooth/verify |
Challenge verification endpoint |
challenge_handler |
SHA256Balloon |
Challenge implementation |
blocklist |
None |
IPBlocklist 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 |
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 blocklist |
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 (balloon hashing)
SHA256, # lightweight SHA-256 PoW
CharacterCaptcha, # text CAPTCHA
ImageCaptcha, # image CAPTCHA (requires Pillow)
RotationCaptcha, # rotation CAPTCHA (requires Pillow, numpy)
SlidingCaptcha, # sliding puzzle (requires Pillow)
CircleCaptcha, # circle select CAPTCHA (requires Pillow)
ImageGridCaptcha, # image grid CAPTCHA (requires Pillow)
AudioCaptcha, # audio CAPTCHA (requires numpy, scipy)
NavigatorAttestation, # browser signal attestation
ThirdPartyCaptchaChallenge, # embed external CAPTCHAs
)
vouch = Vouch(app, secret="s", challenge_handler=CharacterCaptcha())
IP blocklist
from flask_vouch import Vouch, IPBlocklist
bl = IPBlocklist() # defaults to bundled blocklist URL
bl.load()
bl.start_updates() # auto-refresh daily in a daemon thread
vouch = Vouch(app, secret="s", blocklist=bl)
Multiple blocklists:
vouch = Vouch(app, secret="s", blocklist=[bl1, bl2])
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.init_flask(app)
@app.route("/login")
@rl.limit("5/minute")
def login(): ...
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file flask_vouch-1.0.1.tar.gz.
File metadata
- Download URL: flask_vouch-1.0.1.tar.gz
- Upload date:
- Size: 111.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f485f64c9120225dcc1a65785278c0b0b709e67e4331fc57f309ce7da10ede6
|
|
| MD5 |
57ebfa69b51f200ca66e0dc297d6191f
|
|
| BLAKE2b-256 |
7e1ffaaf1723a5d163472cca5b77cd85bf03ba96a401cc7df3d407e8ba6805cc
|
Provenance
The following attestation bundles were made for flask_vouch-1.0.1.tar.gz:
Publisher:
publish.yml on tn3w/flask-Vouch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flask_vouch-1.0.1.tar.gz -
Subject digest:
4f485f64c9120225dcc1a65785278c0b0b709e67e4331fc57f309ce7da10ede6 - Sigstore transparency entry: 1274379462
- Sigstore integration time:
-
Permalink:
tn3w/flask-Vouch@1a8d923ae3eb40c501cb48a813e1839b67af6404 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/tn3w
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1a8d923ae3eb40c501cb48a813e1839b67af6404 -
Trigger Event:
push
-
Statement type:
File details
Details for the file flask_vouch-1.0.1-py3-none-any.whl.
File metadata
- Download URL: flask_vouch-1.0.1-py3-none-any.whl
- Upload date:
- Size: 128.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
208c60c3376cf2186bf05580411d686a341c1f5ffd51ff6fbf191bac18e16583
|
|
| MD5 |
a13980a6a1cfa2e28fe4b0c581fca36e
|
|
| BLAKE2b-256 |
4fd87ee04297342162a63a88a3195539ec26becf053aae443cf7da04fbf8ea87
|
Provenance
The following attestation bundles were made for flask_vouch-1.0.1-py3-none-any.whl:
Publisher:
publish.yml on tn3w/flask-Vouch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flask_vouch-1.0.1-py3-none-any.whl -
Subject digest:
208c60c3376cf2186bf05580411d686a341c1f5ffd51ff6fbf191bac18e16583 - Sigstore transparency entry: 1274379542
- Sigstore integration time:
-
Permalink:
tn3w/flask-Vouch@1a8d923ae3eb40c501cb48a813e1839b67af6404 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/tn3w
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1a8d923ae3eb40c501cb48a813e1839b67af6404 -
Trigger Event:
push
-
Statement type: