# How to Add Authentication to a Python Backend - Part 2

> Part 2 of 3. Start with [How to Add Authentication to a Python Backend](https://clerk.com/articles/how-to-add-authentication-to-a-python-backend.md).

This is the second part of a comprehensive guide on adding Clerk authentication to Python backends. In Part 1, we covered the architectural theory and Clerk Dashboard setup.

Here, we implement the backend authentication layer. We provide complete integrations for the three most popular Python web frameworks: FastAPI, Flask, and Django (via Django REST Framework). Each framework section builds a robust authentication pipeline covering project setup, dependency/middleware design, endpoint protection, and Role-Based Access Control (RBAC).

## Adding Clerk authentication to FastAPI

This is the biggest section. FastAPI's dependency injection system is the idiomatic place for authentication, and the modern `Annotated[X, Depends(dep)]` syntax makes it read cleanly. If you're on an older FastAPI tutorial using `= Depends()` in default parameters, the pattern here is the current one.

### Project setup from scratch

```bash
uv init python-backend-auth
cd python-backend-auth
uv add fastapi "uvicorn[standard]" clerk-backend-api pydantic-settings python-dotenv
```

Minimal `app/main.py`:

```python
from fastapi import FastAPI

app = FastAPI(title="Python Backend Auth")

@app.get("/health")
def health():
    return {"status": "ok"}
```

Run the dev server:

```bash
uv run uvicorn app.main:app --reload --port 8000
```

Hit `http://localhost:8000/health` and you should see `{"status":"ok"}`. Everything else in this section builds on this skeleton.

For production, don't run `uvicorn --reload`. Use Gunicorn as a process manager with Uvicorn workers — `uv add gunicorn uvicorn-worker`, then `gunicorn -k uvicorn_worker.UvicornWorker app.main:app`. Note the `uvicorn_worker` package (hyphen-to-underscore) — the `uvicorn.workers` stdlib module is deprecated in favor of the [standalone `uvicorn-worker` package](https://pypi.org/project/uvicorn-worker/).

### Configure `pydantic-settings`

Create `app/config.py`:

```python
from functools import lru_cache
from typing import Annotated

from pydantic import field_validator
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict


class Settings(BaseSettings):
    clerk_secret_key: str
    clerk_jwt_key: str | None = None
    clerk_authorized_parties: Annotated[list[str], NoDecode] = []
    clerk_webhook_signing_secret: str | None = None

    @field_validator("clerk_authorized_parties", mode="before")
    @classmethod
    def _split_csv(cls, v: str | list[str]) -> list[str]:
        if isinstance(v, str):
            return [p.strip() for p in v.split(",") if p.strip()]
        return v

    model_config = SettingsConfigDict(env_file=".env", case_sensitive=False)


@lru_cache
def get_settings() -> Settings:
    return Settings()
```

Two details worth understanding here.

First, the `NoDecode` + `field_validator` pair prevents `pydantic-settings` from treating a CSV list as JSON (its default for `list[str]`), letting you use standard `CLERK_AUTHORIZED_PARTIES='http://localhost:3000'` formatting.

Second, `@lru_cache` ensures `.env` is read once, and allows clean testing via `app.dependency_overrides`.

### Create the Clerk authentication dependency

This is the subsection to read twice. Everything else builds on it.

Create `app/auth.py`:

```python
from typing import Annotated

from clerk_backend_api import AuthenticateRequestOptions, authenticate_request
from clerk_backend_api.security.types import RequestState
from fastapi import Depends, HTTPException, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

from app.config import Settings, get_settings

http_bearer = HTTPBearer(auto_error=False)


def require_auth(
    request: Request,
    settings: Annotated[Settings, Depends(get_settings)],
    _creds: Annotated[HTTPAuthorizationCredentials | None, Depends(http_bearer)] = None,
) -> RequestState:
    state = authenticate_request(
        request,
        AuthenticateRequestOptions(
            secret_key=settings.clerk_secret_key,
            jwt_key=settings.clerk_jwt_key,
            authorized_parties=settings.clerk_authorized_parties,
            accepts_token=["session_token"],
        ),
    )
    if not state.is_signed_in:
        raise HTTPException(
            status_code=401,
            detail=state.reason.name if state.reason else "unauthorized",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return state
```

A few decisions baked into this dependency are worth explaining.

**`HTTPBearer(auto_error=False)`**, not `OAuth2PasswordBearer`. You aren't running an OAuth server; this provides the Swagger "Authorize" button and lets Clerk's SDK emit specific rejection reasons instead of generic 403s.

**Pass `Request` directly into `authenticate_request()`.** FastAPI's `Request` is a Starlette object with a `headers` mapping. Clerk's [`Requestish`](https://raw.githubusercontent.com/clerk/clerk-sdk-python/main/src/clerk_backend_api/security/types.py) protocol is structurally satisfied — you don't wrap it, don't convert, don't `.dict()` it.

**Module-level `authenticate_request`**. Explicit, avoids context managers, and matches established patterns. (The instance method `sdk.authenticate_request()` works too).

**Networkless with `jwt_key`**. Using the PEM public key verifies signatures locally, which is faster and more resilient than the networked JWKS fallback.

**Explicit `authorized_parties`.** The [Manual JWT verification guide](https://clerk.com/docs/guides/sessions/manual-jwt-verification.md) says: "Neglecting to validate `azp` can expose your application to CSRF attacks." The list is your allow-list of frontend origins. It's a single line of defense, so don't skip it.

**Explicit `accepts_token=["session_token"]`**. The Python SDK accepts any token type by default. Pass this explicitly to restrict endpoints to user sessions.

**Return `RequestState`, not `state.payload`.** The full state includes `is_signed_in` (and its alias `is_authenticated`), `status`, `reason`, `payload`, `token`, and `to_auth()`. Downstream dependencies (`require_permission`, `current_user`) want all of it.

### Protecting different types of endpoints

Three patterns you'll need: public, authenticated, and permission-gated.

Public endpoints need no dependency at all. Create `app/routers/public.py`:

```python
from fastapi import APIRouter

router = APIRouter()

@router.get("/health")
def health():
    return {"status": "ok"}

@router.get("/api/public/posts")
def public_posts():
    return {"posts": [{"id": 1, "title": "Hello, world"}]}
```

Authenticated endpoints inject `require_auth`. Create `app/routers/protected.py`:

```python
from typing import Annotated

from clerk_backend_api.security.types import RequestState
from fastapi import APIRouter, Depends

from app.auth import require_auth

router = APIRouter(prefix="/api", tags=["protected"])

@router.get("/me")
def me(state: Annotated[RequestState, Depends(require_auth)]):
    return {
        "user_id": state.payload["sub"],
        "session_id": state.payload.get("sid"),
    }
```

Admin-only endpoints stack a permission check on top. We'll build the `require_permission` factory in the next subsection. For now, the shape:

```python
@router.get("/admin/users")
def list_admin_users(
    _: Annotated[None, Depends(require_permission("org:admin:manage"))],
):
    return {"users": []}
```

A note on [roles](https://clerk.com/glossary.md#roles) and permissions: The v2 session token carries the user's system role in the `o.rol` claim, enriched as `payload["org_role"]`. Custom permissions are compactly serialized across three claims (`fea`, `o.per`, `o.fpm`) and decoded into `payload["org_permissions"]`. System permissions are not serialized. See the RBAC section for details. Sources: [session tokens guide](https://clerk.com/docs/guides/sessions/session-tokens.md), [roles and permissions guide](https://clerk.com/docs/guides/organizations/control-access/roles-and-permissions.md).

### Accessing user context inside endpoints

Reading the user ID is a single claim lookup:

```python
user_id = state.payload["sub"]
```

That's enough for 90% of endpoints. The session token already has the user ID, organization ID, and custom permissions. You do not need to make a Backend API call to learn who the user is.

When you need profile data (name, email, metadata), call `sdk.users.get(user_id=user_id)`. Construct the SDK client once at module scope so you reuse its connection pool:

```python
from clerk_backend_api import Clerk
from functools import lru_cache

from app.config import get_settings

@lru_cache
def get_clerk() -> Clerk:
    return Clerk(bearer_auth=get_settings().clerk_secret_key)
```

Then in an endpoint:

```python
from typing import Annotated
from clerk_backend_api import Clerk
from clerk_backend_api.security.types import RequestState
from fastapi import APIRouter, Depends

from app.auth import require_auth

router = APIRouter(prefix="/api")

@router.get("/me/profile")
def profile(
    state: Annotated[RequestState, Depends(require_auth)],
    clerk: Annotated[Clerk, Depends(get_clerk)],
):
    user = clerk.users.get(user_id=state.payload["sub"])
    email = next(
        (address.email_address for address in user.email_addresses or []
         if address.id == user.primary_email_address_id),
        None,
    )
    return {
        "id": user.id,
        "email": email,
        "public_metadata": user.public_metadata,
    }
```

Because FastAPI caches dependency results per request (`use_cache=True` default), adding `state` to multiple sub-dependencies doesn't cost extra calls.

**Bridging auth to your own database.** The `state.payload["sub"]` value is the Clerk user ID (`user_xxxxxxxxxxxx`). Use it as a foreign key into your own tables. A minimal SQLAlchemy 2.0 async lookup:

```python
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models import User

async def load_user(session: AsyncSession, clerk_id: str) -> User | None:
    result = await session.execute(
        select(User).where(User.clerk_id == clerk_id)
    )
    return result.scalar_one_or_none()
```

The column name is `clerk_id`, matching Clerk's [syncing guide](https://clerk.com/docs/guides/development/webhooks/syncing.md). Not `clerk_user_id`, and not `external_id` — `external_id` is a distinct Clerk concept for importing third-party user IDs onto the Clerk user object. The `User` model itself is defined next to the webhook sync handler in [Part 3](https://clerk.com/articles/how-to-add-authentication-to-a-python-backend-3.md#webhook-handling-in-python). SQLModel users can write the same thing with identical `Mapped` syntax.

### Role-based and permission-based access control

The two [RBAC](https://clerk.com/glossary.md#role-based-access-control-rbac) patterns worth knowing: custom permissions (in JWT, free) and system-role lookups (Backend API, slower).

**Custom permissions** use the `org:<feature>:<permission>` format. v2 session tokens encode permissions compactly across `fea`, `o.per`, and `o.fpm` claims. The Python SDK decodes this into a list at `payload["org_permissions"]`. Membership testing is simple:

```python
from typing import Annotated
from clerk_backend_api.security.types import RequestState
from fastapi import Depends, HTTPException

from app.auth import require_auth

def require_permission(permission: str):
    def _check(state: Annotated[RequestState, Depends(require_auth)]) -> RequestState:
        org_permissions = state.payload.get("org_permissions") or []
        if permission not in org_permissions:
            raise HTTPException(status_code=403, detail=f"missing permission: {permission}")
        return state
    return _check
```

Use it in a route:

```python
@router.post("/invoices")
def create_invoice(
    _: Annotated[RequestState, Depends(require_permission("org:invoices:create"))],
):
    return {"ok": True}
```

> Do not split `o.per` on commas and compare directly to `org:invoices:create`. `o.per` contains bare names like `manage,read` — the `org:<feature>:` prefix is reconstructed from `fea`/`o.fpm`, not stored in `o.per`. Using the reconstructed `payload["org_permissions"]` list is the correct way to check permissions server-side. If you're curious about the exact decode logic, see [`_compute_org_permissions` in `authenticaterequest.py`](https://raw.githubusercontent.com/clerk/clerk-sdk-python/main/src/clerk_backend_api/security/authenticaterequest.py), which mirrors the JavaScript SDK's [`buildOrgPermissions`](https://github.com/clerk/javascript/blob/main/packages/shared/src/jwtPayloadParser.ts).

**System roles** live in the `o.rol` claim for the user's active organization, without the `org:` prefix (e.g., `"admin"`, `"member"`). The Python SDK copies that value to `payload["org_role"]` during `authenticate_request()` — see [`_process_payload` in `authenticaterequest.py`](https://raw.githubusercontent.com/clerk/clerk-sdk-python/main/src/clerk_backend_api/security/authenticaterequest.py). So the role check is local, with no network call:

```python
def require_system_role(role: str):
    """Check role for the user's ACTIVE organization (the one in `o.rol`).

    Pass the role without the `org:` prefix (e.g., `"admin"`, `"member"`) to
    match what Clerk stores in the claim.
    """
    def _check(
        state: Annotated[RequestState, Depends(require_auth)],
    ) -> RequestState:
        if not state.payload.get("org_id"):
            raise HTTPException(status_code=403, detail="no organization context")
        if state.payload.get("org_role") != role:
            raise HTTPException(status_code=403, detail=f"missing role: {role}")
        return state
    return _check
```

Use it in a route:

```python
@router.delete("/api/org/members/{member_id}")
def remove_member(
    member_id: str,
    _: Annotated[RequestState, Depends(require_system_role("admin"))],
):
    ...
```

`o.rol` only carries the role for the **active** organization (`o.id`). To check roles in other organizations or enumerate memberships, fall back to the Backend API:

```python
memberships = clerk.users.get_organization_memberships(user_id=state.payload["sub"])
```

Prefer the local `payload["org_role"]` check whenever you're authorizing against the active org — it's faster and doesn't depend on the Backend API being reachable.

> **System permissions** (`org:sys_memberships:manage`, `org:sys_profile:manage`, etc.) are _not_ serialized into the token. If you need to authorize against a system permission on the server, create a custom permission in the Dashboard, assign it to the system role you want to check, and then look for it in `payload["org_permissions"]`. Source: [roles and permissions guide](https://clerk.com/docs/guides/organizations/control-access/roles-and-permissions.md) — "System Permissions aren't included in session claims."

Source: [roles and permissions](https://clerk.com/docs/guides/organizations/control-access/roles-and-permissions.md), [session token reference](https://clerk.com/docs/guides/sessions/session-tokens.md).

### Handling authentication errors gracefully

When `is_signed_in` is `False`, `state.reason` is an enum member whose `.name` is a machine-readable code. The [security/types.py](https://raw.githubusercontent.com/clerk/clerk-sdk-python/main/src/clerk_backend_api/security/types.py) source has the full enum:

1. `SESSION_TOKEN_MISSING` — no token on the request
2. `TOKEN_EXPIRED` — expired JWT
3. `TOKEN_INVALID_SIGNATURE` — bad signature
4. `TOKEN_INVALID_AUTHORIZED_PARTIES` — `azp` not in your allow-list
5. `TOKEN_INVALID` — malformed or otherwise unverifiable token
6. `TOKEN_TYPE_NOT_SUPPORTED` — got an M2M token on a session-only endpoint

Map them consistently. A custom exception handler for a uniform JSON shape:

```python
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse

# in app/main.py, after `app = FastAPI(...)`
@app.exception_handler(HTTPException)
def http_exception_handler(request: Request, exc: HTTPException):
    errors = {401: "unauthorized", 403: "forbidden"}
    return JSONResponse(
        status_code=exc.status_code,
        content={"error": errors.get(exc.status_code, "error"),
                 "reason": exc.detail},
        headers=exc.headers or {},
    )
```

Register it in `app/main.py` (or pass `app` into a helper) so you don't import `app.main` from a module that `main.py` itself imports. The handler is app-wide — any `HTTPException` flows through it, which is why the non-auth statuses fall back to a generic label instead of `"forbidden"`.

Always include `WWW-Authenticate: Bearer` on 401s for RFC 6750 compliance. Never log the raw `Authorization` header or token; log the rejection reason, path, and user ID to avoid CWE-532 vulnerabilities.

### Testing your FastAPI endpoints

The sync `TestClient` still works and is the simplest default:

```python
import pytest
from clerk_backend_api.security.types import AuthStatus, RequestState
from fastapi.testclient import TestClient

from app.auth import require_auth
from app.main import app

def _fake_auth() -> RequestState:
    return RequestState(
        status=AuthStatus.SIGNED_IN,
        payload={"sub": "user_fake123", "sid": "sess_fake"},
    )

@pytest.fixture(autouse=True)
def _override_auth():
    app.dependency_overrides[require_auth] = _fake_auth
    yield
    app.dependency_overrides = {}

def test_me_endpoint():
    client = TestClient(app)
    response = client.get("/api/me")
    assert response.status_code == 200
    assert response.json() == {"user_id": "user_fake123", "session_id": "sess_fake"}

def test_unauthenticated_returns_401():
    app.dependency_overrides = {}
    client = TestClient(app)
    response = client.get("/api/me")
    assert response.status_code == 401
```

Overriding `require_auth` at the dependency level is cleaner than patching `authenticate_request` globally, leaving the rest of the request chain intact. The override returns a real `RequestState` (setting `status=AuthStatus.SIGNED_IN` is enough — `is_signed_in` is a computed property), so endpoints that read `state.status`, `state.token`, or `state.reason`, not just `state.payload`, behave as they do in production.

For async tests that need to `await` something (async DB sessions, an external API), switch to `httpx.AsyncClient` with `ASGITransport`:

```python
import pytest
from httpx import ASGITransport, AsyncClient

from app.main import app

@pytest.mark.asyncio
async def test_me_async():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        response = await ac.get("/api/me")
    assert response.status_code == 200
```

Add [`pytest-asyncio`](https://pypi.org/project/pytest-asyncio/) (1.3.0+ on PyPI, 2025-11-10) and put this in `pyproject.toml`:

```toml
[tool.pytest_asyncio]
asyncio_mode = "auto"
```

With `asyncio_mode = "auto"`, every `async def test_*` function is auto-marked — you don't have to sprinkle `@pytest.mark.asyncio` on each one. `@pytest.mark.anyio` is FastAPI's own preferred spelling (it can run the same test under both asyncio and trio) and is equivalent for an asyncio-only project.

One gotcha: `AsyncClient + ASGITransport` does _not_ trigger FastAPI's lifespan events (startup and shutdown). If your tests depend on startup hooks (database connection pools, etc.), wrap with [`asgi-lifespan`](https://pypi.org/project/asgi-lifespan/)'s `LifespanManager`:

```python
from asgi_lifespan import LifespanManager

@pytest.mark.asyncio
async def test_with_lifespan():
    async with LifespanManager(app):
        async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
            response = await ac.get("/api/me")
```

Avoid over-mocking to catch token parsing bugs. For integration tests, use a real dev session token from `getToken()`. Sources: [FastAPI testing dependencies](https://fastapi.tiangolo.com/advanced/testing-dependencies/), [FastAPI async tests](https://fastapi.tiangolo.com/advanced/async-tests/), [pytest-asyncio concepts](https://pytest-asyncio.readthedocs.io/en/stable/concepts.html).

## Adding Clerk authentication to Flask

Parallel section to FastAPI. Flask is simpler, less opinionated, and the integration is a decorator instead of a dependency. If you're coming from FastAPI, the patterns map one-to-one; only the syntax changes.

### Project setup from scratch

```bash
uv init python-flask-auth
cd python-flask-auth
uv add flask clerk-backend-api python-dotenv
```

Minimal `app/__init__.py`:

```python
import os
from flask import Flask
from dotenv import load_dotenv

load_dotenv()


def env_csv(key: str, default: list[str] | None = None) -> list[str]:
    raw = os.environ.get(key)
    if not raw:
        return default or []
    return [p.strip() for p in raw.split(",") if p.strip()]


def create_app() -> Flask:
    app = Flask(__name__)
    app.config.update(
        CLERK_SECRET_KEY=os.environ["CLERK_SECRET_KEY"],
        CLERK_JWT_KEY=os.environ.get("CLERK_JWT_KEY"),
        CLERK_AUTHORIZED_PARTIES=env_csv("CLERK_AUTHORIZED_PARTIES"),
        CLERK_WEBHOOK_SIGNING_SECRET=os.environ.get("CLERK_WEBHOOK_SIGNING_SECRET"),
    )

    @app.get("/health")
    def health():
        return {"status": "ok"}

    from app.routes.protected import bp as protected_bp
    app.register_blueprint(protected_bp)

    return app
```

Run the dev server:

```bash
uv run flask --app app run --debug --port 5000
```

For production, add Gunicorn (`uv add gunicorn`), then: `gunicorn "app:create_app()" -w 4 -b 0.0.0.0:8000`. The `-w $((2*$(nproc)+1))` formula from the Gunicorn docs is a reasonable default for I/O-bound Python apps. Source: [Flask testing docs](https://flask.palletsprojects.com/en/stable/testing/).

### The Flask auth decorator

Flask's canonical pattern for cross-cutting concerns is a decorator. Build it once, use it everywhere.

Create `app/auth.py`:

```python
from functools import wraps
from clerk_backend_api import AuthenticateRequestOptions, authenticate_request
from flask import abort, current_app, g, request


def clerk_required(view):
    @wraps(view)
    def wrapper(*args, **kwargs):
        state = authenticate_request(
            request,
            AuthenticateRequestOptions(
                secret_key=current_app.config["CLERK_SECRET_KEY"],
                jwt_key=current_app.config.get("CLERK_JWT_KEY"),
                authorized_parties=current_app.config["CLERK_AUTHORIZED_PARTIES"],
                accepts_token=["session_token"],
            ),
        )
        if not state.is_signed_in:
            abort(401, description=state.reason.name if state.reason else "unauthorized")
        g.auth_state = state
        g.user_id = state.payload["sub"]
        return view(*args, **kwargs)
    return wrapper
```

A few Flask-specific decisions worth calling out.

**Flask's `request` satisfies `Requestish`.** Pass it directly without wrapping; `EnvironHeaders` is a valid mapping.

**`g` is Flask's per-request namespace.** Thread-safe and scoped to the request, it's the ideal place for `g.auth_state` and `g.user_id`.

**`functools.wraps`**. Always wrap, or Flask routing breaks due to name collisions.

**`env_csv` in `create_app`.** Flask has no built-in type coercion, so we convert the `CLERK_AUTHORIZED_PARTIES` string to a list once at startup. The naive `os.environ.get("CLERK_AUTHORIZED_PARTIES", "").split(",")` returns `[""]` on an unset var — which doesn't actually fail the Clerk `azp` check (the empty list would), but it leaks an empty string into the comparison and makes debugging harder. Trim and filter.

### Alternative: `before_request` global middleware

When almost every route is protected, a decorator on every view feels noisy. Global middleware via `before_request` is cleaner.

```python
from flask import g, request, abort
from clerk_backend_api import AuthenticateRequestOptions, authenticate_request


PUBLIC_ENDPOINTS = {"static", "health", "webhooks.clerk_webhook", "protected.public_posts"}


def register_auth_middleware(app):
    @app.before_request
    def _authenticate():
        if request.endpoint in PUBLIC_ENDPOINTS:
            return None
        state = authenticate_request(
            request,
            AuthenticateRequestOptions(
                secret_key=app.config["CLERK_SECRET_KEY"],
                jwt_key=app.config.get("CLERK_JWT_KEY"),
                authorized_parties=app.config["CLERK_AUTHORIZED_PARTIES"],
                accepts_token=["session_token"],
            ),
        )
        if not state.is_signed_in:
            abort(401, description=state.reason.name if state.reason else "unauthorized")
        g.auth_state = state
        g.user_id = state.payload["sub"]
```

Call `register_auth_middleware(app)` inside `create_app()`. The allow-list uses `request.endpoint` (the Flask route name) rather than `request.path` (`/health`) because endpoint names are more stable when you add URL prefixes. Watch the naming: routes registered on a blueprint get the blueprint's name as a prefix — the webhook view from Part 3 is `webhooks.clerk_webhook` and the public posts view below is `protected.public_posts`, while app-level routes like `health` are unprefixed. An endpoint missing from the set gets auth by default, so a typo here fails closed (a 401), not open.

Per-route decorator vs. global middleware is a tradeoff. Prefer the decorator if most routes are public. Prefer global middleware if most routes are protected and you want auth-by-default. Don't mix both in the same app — it becomes hard to reason about which path ran which check.

### Protecting different types of endpoints

Create `app/routes/protected.py`:

```python
from flask import Blueprint, g, jsonify

from app.auth import clerk_required, require_permission

bp = Blueprint("protected", __name__, url_prefix="/api")


@bp.get("/public/posts")
def public_posts():
    return jsonify({"posts": [{"id": 1, "title": "Hello, world"}]})


@bp.get("/me")
@clerk_required
def me():
    return jsonify({"user_id": g.user_id})


@bp.get("/admin/users")
@clerk_required
@require_permission("org:admin:manage")
def list_admin_users():
    return jsonify({"users": []})
```

Decorator order matters. Route registration (`@bp.get`) must be outermost, followed by `@clerk_required`, then `@require_permission`. Flipping the auth and permission decorators causes crashes against uninitialized `g.auth_state`.

### Accessing user context inside view functions

`g.user_id` is already set. For profile data, call `sdk.users.get()` via a cached helper:

```python
from functools import cache
from clerk_backend_api import Clerk
from flask import current_app, g


@cache
def _sdk() -> Clerk:
    return Clerk(bearer_auth=current_app.config["CLERK_SECRET_KEY"])


def current_user():
    if "_current_user" not in g:
        g._current_user = _sdk().users.get(user_id=g.user_id)
    return g._current_user
```

This provides a `current_user()` API similar to Flask-Login, but using Clerk.

**Bridging to your database.** Once the decorator has verified the token, `g.user_id` is the Clerk user ID you join against. A SQLAlchemy 2.0 sync lookup (works with Flask-SQLAlchemy 3.x or plain SQLAlchemy):

```python
from sqlalchemy import select
from app.extensions import db
from app.models import User

@bp.get("/me/subscription")
@clerk_required
def my_subscription():
    user = db.session.scalar(select(User).where(User.clerk_id == g.user_id))
    if not user:
        return {"subscription": "free"}
    return {"subscription": user.subscription_tier}
```

If you're on Flask-SQLAlchemy 2.x, `User.query.filter_by(clerk_id=g.user_id).first()` works, but the 2.0 `select()` style matches what upstream SQLAlchemy will support going forward. Column name is `clerk_id`, per Clerk's [syncing guide](https://clerk.com/docs/guides/development/webhooks/syncing.md). The full `User` model is in [Part 3](https://clerk.com/articles/how-to-add-authentication-to-a-python-backend-3.md#webhook-handling-in-python).

### RBAC and permission checks

Same pattern as FastAPI — read the decoded `payload["org_permissions"]` list that `authenticate_request()` populates from the v2 `fea` / `o.per` / `o.fpm` claims:

```python
from functools import wraps
from flask import g, abort


def require_permission(permission: str):
    def decorator(view):
        @wraps(view)
        def wrapper(*args, **kwargs):
            org_permissions = g.auth_state.payload.get("org_permissions") or []
            if permission not in org_permissions:
                abort(403, description=f"missing permission: {permission}")
            return view(*args, **kwargs)
        return wrapper
    return decorator
```

For system-role checks, read `payload["org_role"]` — same local check as FastAPI. The Python SDK populates it from the v2 `o.rol` claim during `authenticate_request()`, so no Backend API call is needed for the active organization:

```python
def require_system_role(role: str):
    """Pass `role` without the `org:` prefix (e.g., `"admin"`)."""
    def decorator(view):
        @wraps(view)
        def wrapper(*args, **kwargs):
            payload = g.auth_state.payload
            if not payload.get("org_id"):
                abort(403, description="no organization context")
            if payload.get("org_role") != role:
                abort(403, description=f"missing role: {role}")
            return view(*args, **kwargs)
        return wrapper
    return decorator
```

If you need to enumerate every organization the user belongs to (not just the active one), or check a role in a non-active organization, you still fall back to `_sdk().users.get_organization_memberships(user_id=g.user_id)` — that's the only case where a Backend API call is required.

### Error handling for APIs

Flask's default 401 renders HTML. For a JSON API, register a handler:

```python
from flask import jsonify
from werkzeug.exceptions import HTTPException


def register_error_handlers(app):
    @app.errorhandler(HTTPException)
    def _json_errors(exc: HTTPException):
        return jsonify({
            "error": "unauthorized" if exc.code == 401 else
                     "forbidden" if exc.code == 403 else
                     exc.name.lower().replace(" ", "_"),
            "reason": exc.description,
        }), exc.code
```

Register this inside `create_app()`. Same error shape as the FastAPI example, so clients can code against a consistent contract regardless of which Python framework your team landed on.

### Testing Flask endpoints

Flask's [`test_client()`](https://flask.palletsprojects.com/en/stable/testing/) is the idiomatic path. The key design decision: use **two fixtures**, one that exercises the real `clerk_required` decorator (for 401 tests) and one that injects a fake authenticated user (for happy-path tests). The common tempting shortcut — `monkeypatch.setattr("app.auth.clerk_required", ...)` — does _not_ work, because routes captured the original decorator reference at import time and pytest's monkeypatch can't retroactively rebind those references.

The cleanest pattern makes `clerk_required` test-aware via a config flag, and injects the fake user with `@app.before_request`. Update `app/auth.py` to honor the flag:

```python
from flask import current_app, g, request
from functools import wraps

def clerk_required(view):
    @wraps(view)
    def wrapper(*args, **kwargs):
        if current_app.config.get("TESTING_AUTH") and hasattr(g, "user_id"):
            return view(*args, **kwargs)  # trust before_request-injected g
        # ...real authenticate_request() path from "The Flask auth decorator" above...
    return wrapper
```

Then in `tests/conftest.py`:

```python
import pytest
from clerk_backend_api.security.types import AuthStatus, RequestState
from flask import g

from app import create_app


@pytest.fixture
def app():
    """Real app with the real clerk_required decorator wired up."""
    app = create_app()
    app.config.update(TESTING=True)
    return app


@pytest.fixture
def client(app):
    """Anonymous client — no auth injected. Real decorator runs and returns 401."""
    with app.test_client() as c:
        yield c


@pytest.fixture
def authenticated_client(app):
    """Client with a fake authenticated user injected via before_request."""
    app.config["TESTING_AUTH"] = True

    @app.before_request
    def _inject_fake_auth():
        g.auth_state = RequestState(
            status=AuthStatus.SIGNED_IN,
            payload={"sub": "user_fake"},
        )
        g.user_id = "user_fake"

    with app.test_client() as c:
        yield c
```

Now each test picks the fixture that matches the path it wants to exercise:

```python
def test_me_returns_user_id(authenticated_client):
    response = authenticated_client.get("/api/me")
    assert response.status_code == 200
    assert response.get_json() == {"user_id": "user_fake"}


def test_unauthenticated_returns_401(client):
    response = client.get("/api/me")
    assert response.status_code == 401
    assert response.get_json()["error"] == "unauthorized"
```

`client` runs the real decorator (no bypass), so the 401 assertion actually exercises `authenticate_request()`. `authenticated_client` sets `TESTING_AUTH=True` and populates `g` — the decorator short-circuits cleanly, and because the `app` fixture is function-scoped, the hook never leaks across tests.

For integration tests against a real Clerk dev instance, use a session token from a browser session: sign in via a local frontend, call `window.Clerk.session.getToken()` in the browser console, then use that token in `client.get('/api/me', headers={'Authorization': f'Bearer {token}'})`.

## Brief: using Clerk with Django / DRF

Clerk does not ship a first-party Django package in 2026. Avoid outdated community packages like `clerk-django` or `django-clerk`.

The idiomatic Django integration uses Django REST Framework's [`BaseAuthentication`](https://www.django-rest-framework.org/api-guide/authentication/). It returns a `(user, auth)` tuple. A lightweight `ClerkUser` stub with `is_authenticated = True` skips the Django user model entirely.

```python
from dataclasses import dataclass

from clerk_backend_api import AuthenticateRequestOptions, authenticate_request
from clerk_backend_api.security.types import AuthErrorReason
from django.conf import settings
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed


@dataclass
class ClerkUser:
    """Minimal `request.user` for Clerk-authenticated requests.

    Clerk owns the user directory; this object exposes just enough for
    DRF's `IsAuthenticated` permission. If the app also maintains a
    local User row (via Clerk webhooks), look it up by `id` — the Clerk
    `sub` — in the view or a thin helper.
    """

    id: str
    payload: dict

    is_authenticated: bool = True
    is_anonymous: bool = False
    is_active: bool = True

    def __str__(self) -> str:
        return self.id


class ClerkAuthentication(BaseAuthentication):
    def authenticate(self, request):
        state = authenticate_request(
            request,
            AuthenticateRequestOptions(
                secret_key=settings.CLERK_SECRET_KEY,
                jwt_key=settings.CLERK_JWT_KEY,
                authorized_parties=settings.CLERK_AUTHORIZED_PARTIES,
                accepts_token=["session_token"],
            ),
        )
        if not state.is_signed_in:
            if state.reason is AuthErrorReason.SESSION_TOKEN_MISSING:
                return None  # no token supplied; DRF treats this as "not attempted"
            raise AuthenticationFailed(state.reason.name if state.reason else "unauthorized")
        user = ClerkUser(id=state.payload["sub"], payload=state.payload)
        return (user, state)

    def authenticate_header(self, request):
        return "Bearer"
```

If you need to join request data against local rows (profiles, subscriptions, app-specific fields), sync users via Clerk webhooks and look up the local record by the Clerk ID exposed as `request.user.id`. See [Sync Clerk data to your application with webhooks](https://clerk.com/docs/guides/development/webhooks/syncing.md). Clerk's syncing guide explicitly recommends skipping a local user table when you don't need one: _"If you can access the necessary data directly from the Clerk session token, you can achieve strong consistency while avoiding the overhead of maintaining a separate user table."_

A distinction worth internalizing: returning `None` tells DRF the request wasn't attempted with this scheme (so other authenticators — or anonymous access — can proceed), while raising `AuthenticationFailed` rejects a request that _did_ present a token but an invalid one. Collapsing both into `None` would let a request with an expired or forged token fall through as merely anonymous.

Register it in `settings.py`:

```python
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "yourapp.auth.ClerkAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}
```

The `DEFAULT_PERMISSION_CLASSES` line is not optional. DRF authentication only _identifies_ the caller — it never denies a request. Without it, DRF's default permission is `AllowAny` and every endpoint stays public no matter how good the authentication class is. Opt public views back out with `permission_classes = [AllowAny]`.

Permission checks on a `ViewSet`:

```python
from rest_framework.permissions import BasePermission


def has_clerk_permission(permission: str):
    class _HasClerkPermission(BasePermission):
        def has_permission(self, request, view):
            state = request.auth  # the RequestState returned by authenticate()
            if state is None:
                return False
            org_permissions = state.payload.get("org_permissions") or []
            return permission in org_permissions

    return _HasClerkPermission
```

Use it with `permission_classes = [has_clerk_permission("org:invoices:create")]`. The factory shape matters: DRF instantiates each entry in `permission_classes` with no arguments, so a permission class with a required constructor argument can't be registered directly — the closure carries the permission string instead.

For plain Django (not DRF), subclass `MiddlewareMixin` and populate `request.user` from the authenticated state inside `process_request`. Docs: [DRF authentication](https://www.django-rest-framework.org/api-guide/authentication/).

## Conclusion

You now have a complete, production-ready authentication layer in your Python backend. Whether you chose FastAPI's elegant dependencies, Flask's canonical decorators, or Django REST Framework's class-based authentication, your endpoints are secure. You've implemented local JWT verification for performance and handled Role-Based Access Control directly from the token claims.

In the final part of this series, we will bridge this secure foundation to your application's data.

## FAQ

## FAQ

### Can I use Clerk with FastAPI, Flask, and Django?

Yes. The SDK is framework-agnostic — `authenticate_request()` accepts any object with a `headers` mapping, which FastAPI, Flask, Django, Starlette, and Sanic request objects all satisfy.

### Do I need to call Clerk's API on every request to verify a token?

No. Session tokens are RS256 JWTs that verify locally with the public key. Pass `jwt_key=CLERK_JWT_KEY` into `AuthenticateRequestOptions` and every verification is an in-memory signature check.

### How do I test Clerk-protected endpoints locally?

Two patterns. For unit tests, override the auth dependency or patch the decorator to inject a fake user. For integration tests, sign in via a local frontend against a Clerk dev instance and use the real session token in your test requests.

## In this series

1. [How to Add Authentication to a Python Backend](https://clerk.com/articles/how-to-add-authentication-to-a-python-backend.md)
2. **How to Add Authentication to a Python Backend - Part 2** (you are here)
3. [How to Add Authentication to a Python Backend - Part 3](https://clerk.com/articles/how-to-add-authentication-to-a-python-backend-3.md)
