fastapi-better-auth-bridge
A bridge to a TypeScript Better Auth server — not a Python port. (If you want a full Python re-implementation, this is not it.) Community-maintained; not affiliated with or endorsed by Better Auth.
Status. Mode B — a JWT verified against your server's JWKS — is shipped, and every change to it is conformance-tested in CI against a real Better Auth server rather than a mock. Mode A (session cookie + shared session store) ships in one release together with its CSRF protection, never before it; Mode C (remote
get-session) comes after that. No dates are promised.
Modes
Better Auth is TypeScript-only: sign-in/up, OAuth and 2FA run on your Node service. This package makes the sessions that service issues first-class in FastAPI.
| Mode | Status | How | Revocation lag |
|---|---|---|---|
| B — JWT / JWKS | available now | Verify Better Auth JWT-plugin tokens statelessly against /api/auth/jwks (EdDSA by default, pinned algorithm allowlist, required claims, token-lifetime ceiling) |
≤ token lifetime (15 min upstream default) |
| A — Cookie + shared DB/Redis | planned | Verify the signed session_token cookie (HMAC-SHA256, exact wire parity with better-call) and read the session store directly |
Instant |
| C — Remote get-session | planned | Forward the credential to GET /api/auth/get-session with fail-closed semantics |
Instant |
Which better-auth and Python versions each lane exercises: COMPATIBILITY.md.
Install
Python 3.10+. The distribution is fastapi-better-auth-bridge — the shorter spelling collides with
an unrelated package under PyPI's name-similarity rules — and the import is fastapi_better_auth.
JwtVerifier fetches your key set through a pluggable Transport: pick the adapter for the client
your project already has, or install neither extra and pass your own Transport.
| Extra | Installs | Adapter |
|---|---|---|
[httpx] |
httpx>=0.27 |
HttpxTransport, used by default when you pass no transport= |
[httpx2] |
httpx2>=2.0 |
Httpx2Transport |
pip install "fastapi-better-auth-bridge[httpx]" # or: uv add "fastapi-better-auth-bridge[httpx]"
Quickstart (Mode B)
Upstream prerequisite: the JWT plugin has to be
mounted on your Better Auth server (plugins: [jwt()]). It serves /api/auth/jwks, the key set
verified against, and /api/auth/token, where a signed-in client fetches its token; without it
there is nothing here to verify. Then, in FastAPI:
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi_better_auth import BetterAuth, JwtVerifier, Session, User
auth = BetterAuth(verifiers=[JwtVerifier(base_url="https://auth.example.com")])
CurrentSession = Annotated[Session[User], Depends(auth.current_session())]
app = FastAPI()
@app.get("/me")
async def me(session: CurrentSession) -> User:
return session.user
base_url is the whole of the trust configuration: canonicalized once, it is the required iss,
the required aud, and the origin the key set is fetched from — and nothing is derived from the
incoming request. If your deployment already sets BETTER_AUTH_URL for the Node side,
BetterAuth.from_env() reads exactly that one variable, raises if missing, and builds the same:
from fastapi_better_auth import BetterAuth
auth = BetterAuth.from_env()
Call the factory — Depends(auth.current_session()), with the parentheses. Passed bare it would
make the factory itself the dependency, a silent bypass of every route beneath a router, so every
Depends and Security planting of a bare factory is refused with a ConfigurationError while
the route is registered and the application never starts. The one exception is a bare factory
assigned into app.dependency_overrides, a plain dict this library has no hook into: there the
application does start, and the same refusal fires on the first request touching that dependency —
still verifying nothing, and still serving nobody.
/docs needs no wiring: the security scheme is derived from each verifier's own credential source,
so the Authorize button works out of the box.
Your own user model, and optional authentication
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi_better_auth import BetterAuth, JwtVerifier, Session, User
class Member(User):
role: str | None = None
auth = BetterAuth(verifiers=[JwtVerifier(base_url="https://auth.example.com")])
CurrentMember = Annotated[Session[Member], Depends(auth.current_session(user_model=Member))]
MaybeMember = Annotated[Session[Member] | None, Depends(auth.optional_session(user_model=Member))]
app = FastAPI()
@app.get("/role")
async def role(session: CurrentMember) -> str:
return session.user.role or "member"
@app.get("/greeting")
async def greeting(session: MaybeMember) -> str:
return "hello" if session is None else f"hello, {session.user.id}"
optional_session returns None for one situation only: no credential was presented at all. One
that was presented and did not verify still fails — a forged or expired token is never downgraded
to "anonymous".
Errors
Every request-time failure is an HTTPException subclass, so FastAPI answers it with no handler of
yours: 401, the body {"detail": "Not authenticated"}, and a WWW-Authenticate: Bearer header.
Missing, malformed, expired, revoked and "the key set could not be fetched" are byte-identical on
the wire, deliberately — a client must not be able to tell them apart and use the difference to
probe. Two credentials on one request are a 400 ({"detail": "Ambiguous request"}), decided
before anything is verified.
Why a request was refused lives on the exception, as .reason, and nowhere else. This library
does not log ordinary refusals — a forged, expired or malformed token, an unknown key id, a
missing or ambiguous credential — and that is deliberate rather than an omission: what to record
about a failed authentication, and where, is the deployment's decision. If you want them, register
a FastAPI exception handler for SessionError and log exc.reason explicitly; note that
logging.exception() renders str(exc), which does not carry it. A reason holds identifiers and
fingerprints — a key id, a truncated hash — never a raw credential.
Do I need to run a Node service?
Better Auth itself always runs in a Node/TypeScript process — sign-up, sign-in, OAuth, 2FA, and session issuance stay there; this library makes FastAPI a first-class consumer of the sessions it issues. Two topologies:
- You have a JS frontend server (Next.js, Nuxt, SvelteKit, …): no extra service — Better Auth
is already mounted at
/api/auth/*inside the frontend you deploy, and FastAPI verifies what it issues: nothing shared at all for Mode B, a shared Postgres/Redis once Mode A lands. - No JS server (static SPA, mobile app, pure API): deploy one tiny Node service whose only job
is mounting Better Auth, and keep 100% of the business logic in FastAPI. This repository's
conformance harness (
harness/) is exactly that service, in Hono.
Either way the browser or app performs its login flows against Better Auth, then presents the resulting JWT to FastAPI, where this library verifies it.
Why a library instead of the snippet
The hand-rolled verifiers circulating in Better Auth issues split the signed cookie on the wrong
dot, miss the __Secure- name, compare HMACs non-constant-time, and never enforce expiresAt
(upstream's findSession doesn't either — the route layer does, so a bare DB join honours expired
sessions forever). Those are Mode A's details, and Mode A is not shipped; the wire facts behind them
are already captured from a running Better Auth server as golden vectors in tests/vectors/, so
the mode will arrive with them pinned rather than guessed.
The shipped mode is held to the same standard: JwtVerifier refuses an algorithm the token's own
header chose, refuses an unknown kid rather than trying every published key, spells out the five
required claims because PyJWT requires none by default, and refuses a token whose lifetime upstream
would never have minted. A present-but-invalid credential is terminal — no falling through to a
second verifier — and no failure reason ever reaches the client.
License
MIT © Mulugeta Solomon
Release files for fastapi-better-auth-bridge 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fastapi_better_auth_bridge-0.1.0.tar.gz | 49.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastapi_better_auth_bridge-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 109.1 kB
Release files / fastapi_better_auth_bridge-0.1.0.tar.gz
| Download URL | fastapi_better_auth_bridge-0.1.0.tar.gz |
|---|---|
| Size | 49.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6d17f0abb0329cf2a444783161e59065812103cdcd28beb0ea976a4c1cc0b954
|
|
BLAKE2b-256 checksum How to use checksums |
ecb653186e817aef61bb4d70cae9d6b1f8e5d2d03ce1f0db00f1708506850d87
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.
Transparency logRelease files / fastapi_better_auth_bridge-0.1.0-py3-none-any.whl
| Download URL | fastapi_better_auth_bridge-0.1.0-py3-none-any.whl |
|---|---|
| Size | 59.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
972b7ce35f157a0fd1a180b5367aaf0e6bc317bd27f895a401215d730f1d2da0
|
|
BLAKE2b-256 checksum How to use checksums |
d85648fbba26a7a72c642d85e4dacf92dd3fc681fba049e37ac6953b4990bd40
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.
Transparency log