Skip to main content
Articles

How to Add Authentication to a Python Backend

Author: Roy Anger
Published: (last updated )

This is the first part of our guide on adding authentication to a Python backend. We will cover the mental model, your options for Python authentication, and how to set up the Clerk Python SDK.

A Python backend authenticates by verifying a signed token the frontend attaches to every request — it does not render sign-in forms, run OAuth handshakes, or store passwords. The recommended 2026 stack is Clerk's official clerk-backend-api for token verification on the Python side, paired with any Clerk frontend SDK (React, Next.js, Expo, Vanilla JS, iOS, Android) for the sign-in UI. The walkthrough below covers FastAPI and Flask in depth, Django briefly, and the React call-site pattern for completeness.

authenticate_request() accepts any request object that exposes a headers mapping (source) — that covers FastAPI Request, Flask request, Django HttpRequest, Starlette Request, and Sanic Request directly. Raw ASGI scopes or other shapes without a headers attribute need a thin adapter.

Quick reference

PieceWhat it doesWhere it runs
Frontend SDK (@clerk/clerk-react, @clerk/nextjs, etc.)Collects credentials, handles OAuth, mints a session tokenBrowser / mobile
clerk-backend-apiVerifies the token, reads claims, calls the Clerk Backend APIYour Python server
Session token (JWT)Signed proof the user is signed in; 60-second lifetimeSent on every request
JWKS / CLERK_JWT_KEYThe public key used to verify signaturesCached on your server
authenticate_request()Reads the token, verifies it, returns the claimsPer-request in your handler

Jump to your framework:

  1. FastAPI integration
  2. Flask integration
  3. Django / DRF pattern
  4. React frontend call-site

A minimal FastAPI example

Here's the smallest useful protected endpoint. Full code with project scaffolding and error handling appears later.

from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Request
from clerk_backend_api import authenticate_request, AuthenticateRequestOptions
import os

app = FastAPI()

def require_user(request: Request) -> str:
    state = authenticate_request(
        request,
        AuthenticateRequestOptions(
            secret_key=os.environ["CLERK_SECRET_KEY"],
            jwt_key=os.environ.get("CLERK_JWT_KEY"),
            authorized_parties=["http://localhost:3000"],
            accepts_token=["session_token"],
        ),
    )
    if not state.is_signed_in:
        raise HTTPException(status_code=401, detail=state.reason.name if state.reason else "unauthorized")
    return state.payload["sub"]

@app.get("/api/me")
def me(user_id: Annotated[str, Depends(require_user)]):
    return {"user_id": user_id}

That's everything. The frontend calls fetch('/api/me', { headers: { Authorization: 'Bearer <token>' } }) and Clerk's SDK verifies the signature locally against CLERK_JWT_KEY. No network call per request, no session storage, no password hashing. The only thing the backend has to know how to do is verify a signature.

Who this guide is for

This article is for three readers:

  1. Python developers building a FastAPI, Flask, or Django backend who need to protect API endpoints and don't want to write JWT verification from scratch.
  2. React or Next.js developers who already use Clerk on the frontend and need the backend half. You're comfortable with Clerk's React components but haven't touched the Python SDK yet.
  3. Developers new to authentication who want a production-ready setup without rolling their own. You've heard the words JWT, OAuth, and SSO, but you don't want to build any of them.

Assumptions:

  1. Python 3.10 or higher. The current clerk-backend-api (v6.0.1) requires >=3.10 per its pyproject.toml — the floor has been 3.10 since v5.0.0. If you're on 3.9, upgrade Python (recommended) or pin clerk-backend-api<5 (discouraged, predates the current API). Python 3.8 support ended back in v1.8.0.
  2. Familiarity with HTTP and at least one of FastAPI or Flask.
  3. A package manager: uv, pip, or poetry. Examples use uv first, pip second.
  4. A frontend that can acquire a Clerk session token: React, Next.js, Expo, mobile, or a Clerk-aware API client.

How to use this guide. The mental model, options, and setup in this part apply to any Python backend. Read them once. Then skip to your framework in Part 2: FastAPI, Flask, or Django / DRF. Part 3 covers the React frontend integration, advanced SDK features, production deployment, and a comparison of Python auth options — all framework-agnostic.

How Python backend authentication actually works

If you take one thing from this article, take this: the frontend acquires the token, the backend verifies it. That's the whole model. Everything else is plumbing.

Frontend vs. backend responsibilities

The frontend is where the human is. It collects passwords (or a passkey prompt, or an OAuth redirect, or an MFA code), hands those to the auth provider's Frontend API, and receives a signed session token back. It then attaches that token to every API call, typically as Authorization: Bearer <token> or a __session cookie.

The backend never sees the password. The backend never runs the OAuth dance. The backend's only job is to verify that the token is genuine, fresh, and from a party it trusts, then read the claims (who is this user? what org are they in? what permissions do they have?) and authorize the request.

A full request lifecycle with Clerk looks like this:

  1. Browser loads your app.
  2. Clerk's frontend SDK talks to the Clerk Frontend API and mints a session.
  3. User makes an action that calls your Python backend.
  4. Frontend fetches the short-lived session token via getToken() and attaches it to the request.
  5. Python backend receives the request, passes it to authenticate_request().
  6. authenticate_request() verifies the RS256 signature using the cached public key, checks expiry, checks the azp claim against your allow-list, returns the claims.
  7. Your handler authorizes the action and returns a response.

There's no handshake to Clerk on that critical path. With jwt_key (networkless mode), verification is a local RS256 signature check — no network round-trip to Clerk on verification. The networked fallback fetches JWKS per kid and caches the key in memory for five minutes, verifying locally between fetches. See the authenticateRequest reference for the exact behavior.

Five misconceptions worth clearing up first

"Python can handle sign-up and sign-in directly." Not in modern auth, no. Sign-up and sign-in involve OAuth redirects, passkey WebAuthn flows, MFA challenges, session refresh with rolling tokens — all of which live in the browser or mobile client. A Python framework can render a form, but the moment you add Google login, passkeys, or magic links, you've moved that flow into the browser anyway. A real community example: clerk/clerk-sdk-python#59 asks for a CLI sign-in helper, which isn't how Clerk's SDK works.

"Clerk's Python SDK has the same UI components as the React SDK." It does not. clerk-backend-api is backend-only. It verifies tokens, reads user data, manages sessions, and handles webhooks. Components like <SignIn />, <UserButton />, and <OrganizationSwitcher /> ship only in the frontend SDKs (@clerk/clerk-react, @clerk/nextjs, @clerk/expo, etc.). The pairing pattern is simple: use Clerk's frontend SDK on the client, clerk-backend-api on the Python server.

"I need to store passwords or session tokens in my database." No. Clerk stores users, passwords, active sessions, MFA factors, OAuth linkages, and impersonation audit trails. Your Python database only stores your application data, keyed by the Clerk user ID (user_xxx...). Clerk's syncing guide documents the canonical pattern: when a user.created webhook arrives, insert a row with clerk_id=data["id"] and nothing else password-adjacent.

"JWT verification requires calling the auth provider on every request." No. RS256 JWTs are asymmetric. The issuer (Clerk) signs with a private key. You verify with the public key. If you pass jwt_key= into AuthenticateRequestOptions with the PEM-formatted public key, verification is a local math operation — zero network calls. The networked fallback fetches JWKS from https://api.clerk.com/v1/jwks per kid and caches the key in memory for five minutes. Either way, you are not round-tripping to Clerk on every request.

"I have to manage CORS, cookies, and tokens manually." The SDK reads the token automatically. It checks Authorization: Bearer <token> first, then the __session cookie as a fallback. FastAPI's Request and Flask's request both satisfy the Requestish structural protocol the SDK expects — no wrapping, no adapter. You do have to configure CORS on your Python side (covered in Part 3), but token extraction is not something you write.

Token formats a Python backend sees

Clerk emits several token types. Most Python backends only handle the first one, but it's worth knowing the rest exist.

Token typePrefixTransportTypical use
Session JWTnone__session cookie or Authorization: BearerUser-initiated API calls
API keyak_Authorization: BearerUser-created programmatic access
M2M token (opaque)m2m_Authorization: BearerService-to-service, revocable
M2M token (JWT)mt_Authorization: BearerService-to-service, signed JWT format
OAuth access tokenoat_Authorization: BearerThird-party apps acting on behalf of a user

A note on defaults that trips up teams migrating from the Node SDK. In the current Python SDK (6.0.1), the accepts_token field on AuthenticateRequestOptions defaults to ['any'] — every token type above is accepted by default. Clerk's canonical authenticateRequest reference documents the JS/Node SDK default as 'session_token', and the Node SDK enforces that default. For parity with the documented default and defense in depth, pass accepts_token=['session_token'] explicitly on session-only endpoints; restrict M2M-only endpoints with accepts_token=['m2m_token'] or combine types with accepts_token=['session_token', 'api_key']. The full token type definition lives in the SDK source.

One note worth keeping in your pocket: the default session token format is v2. Clerk deprecated v1 on 2025-04-14; in the raw JWT, org claims that used to be flat (org_id, org_role) are now nested under an o object (o.id, o.rol, o.per). The Python SDK smooths this over by re-surfacing the flat names on payload after authenticate_request()payload["org_id"], payload["org_role"], payload["org_slug"], plus the decoded payload["org_permissions"] — so application code can read them directly without touching the nested payload["o"] dict. Copy-pasted code from mid-2024 tutorials that reached into the (now-removed) top-level org_id / org_role JWT claims will still work via payload[...] because of that SDK-level enrichment, but anything that reads straight from the JSON-decoded token still has to go through o.*.

Your options for Python backend authentication

There are three realistic paths. Most teams pick option 3 once they count the real cost of the others.

Option 1: Roll your own with PyJWT + pwdlib

You can, in theory, build authentication yourself. You'd hash passwords (Argon2id is the OWASP 2024 recommendation), mint and verify JWTs, rotate signing keys, send verification emails, handle password resets, implement MFA, integrate passkeys via WebAuthn, wire up OAuth clients for every social provider, rate-limit login endpoints, detect credential stuffing, and commit to a SOC 2 audit cycle.

WorkOS's 2026 Python authentication guide estimates 2–6 weeks for an MVP, 2–3 months for a production-ready system, and $50,000–$200,000 per year for SOC 2 compliance alone. That's before passkeys or organizations.

A critical note on libraries if you're reading older tutorials: the classic FastAPI stack was python-jose + passlib. Both are effectively unmaintained. python-jose carries CVE-2024-33663, an algorithm confusion vulnerability with a CVSS score of 6.5. passlib's last release was October 2020 and it breaks with bcrypt ≥5.0. FastAPI itself officially migrated to PyJWT and pwdlib with Argon2 support in May 2024. If you're going to roll your own, use those instead.

Where rolling your own fits: learning exercises, internal tools with no external users, or cases where auth is literally your product.

Option 2: Framework extensions

Flask-Login is session-cookie based. It doesn't fit a stateless bearer-token API where the frontend and backend are decoupled (a React SPA calling a Python API). The last release was 0.6.3 in October 2023.

FastAPI Users is in maintenance mode. The maintainers have publicly stated they're only accepting security and dependency updates; no new features. A successor project is discussed in the repo but isn't shipping.

django-allauth (currently 65.16.0) is the one framework extension that's still actively maintained and feature-complete. It includes MFA, WebAuthn, 100+ social providers, and email verification. It fits Django apps with server-rendered pages. It does not fit decoupled SPA + API architectures because it's built around Django's session middleware.

None of these give you passkeys plus MFA plus organizations plus webhooks plus prebuilt React UI in one package.

Option 3: Managed auth providers

This is the category Clerk, Auth0, Supabase Auth, Firebase Auth, and AWS Cognito all live in. What they share: hosted user database, prebuilt frontend flows, JWT-based backend verification, SOC 2 compliance, passkey support.

Where they differ matters a lot for Python specifically:

  1. First-party Python SDK maturity and release cadence
  2. Dedicated FastAPI / Flask / Django helpers
  3. Passkey and MFA availability on the free or low tiers
  4. Organizations / multi-tenant B2B support
  5. Networkless JWT verification without DIY JWKS management
  6. Free-tier unit (MRU vs MAU) and allowance
  7. Transparent pricing

Why Clerk is the focus of this guide: first-party clerk-backend-api with monthly releases (6.0.1 in June 2026), strong organizations / B2B support, prebuilt React / Next.js / Expo frontends that match the Python backend one-to-one, passkeys included in the Pro plan, and transparent MRU-based pricing. Python-specific helpers ship in the same SDK: networkless verification, webhook handling, M2M tokens, and organization management are all one import away.

The full comparison table appears in Part 3. Short version: for a new Python API paired with a modern frontend, Clerk is the path with the fewest decisions to make.

Setting up Clerk for a Python backend

Before you touch FastAPI or Flask specifics, get these three things in place: a Clerk application, your keys, and a sane environment config.

Prerequisites

Checklist

Create a Clerk application and collect your keys

Create an application in the Clerk Dashboard. Enable at least one sign-in method (email + password is enough to start; add passkeys, social OAuth, or magic links later).

You'll collect four pieces of configuration:

  1. Publishable key (pk_test_... or pk_live_...) — frontend. Safe to ship in browser bundles. Identifies your Clerk application.
  2. Secret key (sk_test_... or sk_live_...) — backend. Never ships to the client. Authorizes Backend API calls.
  3. JWT public key (PEM) — backend. Used for networkless token verification. Find it on the API keys page: select Show JWT public key and copy the -----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY----- block from the PEM Public Key section of the modal. Confirmed against the Manual JWT verification guide and the authenticateRequest reference.
  4. Webhook signing secret (whsec_...) — backend. Only needed when you add webhooks. We'll cover this in Part 3.

Development vs. production keys matter. Use pk_test_ and sk_test_ locally; switch to pk_live_ and sk_live_ for production deploys. Never share a secret key with the frontend and never commit it to source control.

Install the Clerk Python SDK

uv add clerk-backend-api

Equivalent with pip:

pip install clerk-backend-api

Or poetry:

poetry add clerk-backend-api

Confirm the install: python -c "import clerk_backend_api; print(clerk_backend_api.__version__)" should show 6.0.1 or later. The package is auto-generated from Clerk's OpenAPI spec via Speakeasy, and sync and async variants live on the same Clerk class — there's no separate AsyncClerk. See sdk.py if you're curious about the structure.

Source: clerk-backend-api on PyPI, GitHub repo.

Environment configuration

Create a .env at the project root:

CLERK_SECRET_KEY=sk_test_...
CLERK_JWT_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
CLERK_AUTHORIZED_PARTIES=http://localhost:3000,https://yourapp.com
CLERK_WEBHOOK_SIGNING_SECRET=whsec_...

The CLERK_JWT_KEY value is the PEM block copied from the Dashboard, with real newlines replaced by \n. In FastAPI you'll let pydantic-settings parse it; in Flask you'll use python-dotenv. http://localhost:3000 and https://yourapp.com are placeholders for your real development and production frontend origins — replace both before deploying.

Add .env to your .gitignore:

.env
.env.*
!.env.example

Important

Clerk highly recommends setting authorized_parties when authorizing requests. authenticate_request() compares the token's azp claim against this list; not setting it can open your application to CSRF attacks — a session token issued for yourapp.com could be replayed from evil.com on the same device. The Manual JWT verification guide says verbatim: "For better security, it's highly recommended to explicitly set the authorizedParties option when authorizing requests. … Not setting this value can open your application to CSRF attacks." Configure the list with your real frontend origins before shipping to production.

A common gotcha: CLERK_AUTHORIZED_PARTIES is a string when it lands in os.environ, but AuthenticateRequestOptions expects list[str]. Passing the raw string puts the entire comma-joined value in as a single list element, and every request fails with TOKEN_INVALID_AUTHORIZED_PARTIES. We handle this properly in the FastAPI and Flask configs.

Why CLERK_JWT_KEY is a PEM and not the publishable key: the publishable key identifies your Clerk application to the Frontend API. The JWT public key is the RSA public half of the keypair Clerk uses to sign session tokens. It's what you actually verify signatures with. They're different values, not interchangeable. The Clerk docs also match these names in their canonical environment variables guide. Note: the archived clerk/fastapi-example uses CLERK_API_SECRET_KEY instead of CLERK_SECRET_KEY. The docs and every other Clerk SDK use CLERK_SECRET_KEY. If you're copy-pasting from the archived example, rename the variable.

Two parallel layouts depending on framework.

FastAPI:

app/
  main.py          # FastAPI() + CORS + router registration
  config.py        # Settings(BaseSettings) + get_settings()
  auth.py          # require_auth dependency, require_permission factory
  routers/
    public.py
    protected.py
  webhooks.py      # Clerk webhook endpoint
tests/
.env
pyproject.toml

Flask:

app/
  __init__.py      # create_app() factory + CORS + blueprint registration
  config.py        # Config class reading from os.environ
  auth.py          # @clerk_required decorator + @require_permission
  routes/
    public.py      # Blueprint
    protected.py   # Blueprint
  webhooks.py      # Blueprint with raw-body handler
tests/
.env
pyproject.toml

The exact code for each file appears in the framework sections in Part 2. The layout is not load-bearing; use what fits your team.

Next steps

In this first part, we covered the core concepts of Python backend authentication, explored the available options, and set up the Clerk SDK. In the next part, we will dive into integrating Clerk with FastAPI, Flask, and Django, demonstrating how to protect your endpoints and manage users.

FAQ