Your backend
Flask
Read the signed-in user on every request, protect views with a decorator, serve server-rendered pages and receive webhooks in Flask.
pip install "zevauth[flask]"
This page covers what Flask adds. The client, the backend API and the rules behind every refusal are on Python.
Setup
import os
from flask import Flask
from zevauth import ZevAuth
from zevauth.flask import ZevAuthFlask
zevauth = ZevAuth(
os.environ["ZEVAUTH_ENVIRONMENT_ID"],
secret_key=os.environ["ZEVAUTH_SECRET_KEY"],
)
app = Flask(__name__)
ZevAuthFlask(app, zevauth)
With an app factory, create ZevAuthFlask() once and call
init_app(app, zevauth) inside the factory.
The extension sets g.zevauth on every request: the signed-in person, or
nobody. It never raises.
Protecting views
from flask import g
from zevauth.flask import require_auth
@app.get("/api/me")
@require_auth
def me():
return {"userId": g.zevauth.user_id}
@app.post("/api/payouts")
@require_auth(require_mfa=True)
def payouts():
...
A refused request answers with the same JSON every ZevAuth library uses:
unauthorized (401), session_task_pending (403, with pendingTasks)
and mfa_required (403).
Who is signed in
from zevauth.flask import current_auth
auth = current_auth() # the same object as g.zevauth
In Jinja templates: {% if g.zevauth.is_signed_in %}.
Server-rendered pages
Turn the session bridge on and the extension
registers POST /zevauth/session and GET /zevauth/handshake:
zevauth = ZevAuth(
os.environ["ZEVAUTH_ENVIRONMENT_ID"],
publishable_key=os.environ["ZEVAUTH_PUBLISHABLE_KEY"],
session_bridge=True,
)
It then reads the session cookie on every request. When it has expired, a
page request is redirected through the handshake, which refreshes it in
the browser and returns. Flask-WTF’s CSRF protection does not need to
cover the bridge endpoint: the bridge checks its own header and the
Origin. If you use CSRFProtect, exempt the zevauth blueprint.
Webhooks
from flask import request
from zevauth import ZevAuthWebhookError, construct_webhook_event
@app.post("/webhooks/zevauth")
def zevauth_webhook():
try:
event = construct_webhook_event(
request.get_data(), # the raw bytes
request.headers.get("zevauth-signature"),
os.environ["ZEVAUTH_WEBHOOK_SECRET"],
)
except ZevAuthWebhookError:
return "", 400
...
return "", 204
Read request.get_data(), not request.get_json(): the signature covers
the exact bytes.
Backend API
user = zevauth.api.users.get(g.zevauth.user_id) Updated at, Sunday, November 1, 2026