ZevAuth
Sign up

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