Framework-agnostic authentication for Python, ported from better-auth, with a FastAPI integration.
Project description
better-auth-server
Authentication for Python, ported from better-auth. Ships with a FastAPI integration.
Your users, sessions and accounts live in your own database. There is no hosted service to depend on and no per-user pricing, and the API surface is the one the TypeScript original has proven in production.
from better_auth import BetterAuth, EmailAndPassword
from better_auth.integrations.fastapi import BetterAuthFastAPI
from fastapi import Depends, FastAPI
auth = BetterAuth(
secret="...", # openssl rand -base64 32
base_url="http://localhost:8000",
email_and_password=EmailAndPassword(enabled=True),
)
app = FastAPI()
ba = BetterAuthFastAPI(auth)
app.include_router(ba.router) # mounts /api/auth/*
@app.get("/me")
async def me(result: dict = Depends(ba.require_session)):
return result["user"]
These twenty lines are a working auth server. Sign-up, sign-in, sessions, sign-out, password reset, email verification and social login are mounted under /api/auth, with the same routes, JSON shapes and error codes as better-auth.
Features
- Email and password: sign-up, sign-in, change/set/verify password, reset flow, email verification.
- Social sign-in (OAuth2/OIDC): GitHub, Google and Discord built in, custom providers in a few lines. PKCE, single-use database-backed state, and account linking guarded by provider email verification.
- Sessions in your database: HMAC-signed cookies, sliding expiry (
expires_in/update_age),rememberMe, list and revoke endpoints, bearer tokens for API clients. - Two adapters out of the box: in-memory for dev and tests, SQLAlchemy 2 async for SQLite, PostgreSQL and MySQL (SQLModel engines work as-is). A custom adapter is five methods.
- Plugins can add routes, extend the database schema, and hook before and after every request.
- Secure defaults: scrypt password hashing, CSRF origin checks, open-redirect protection on every
callbackURL, timing-equalized sign-in, rate limiting with better-auth's per-path rules. - The core is framework-agnostic. The FastAPI layer is about 80 lines over plain request/response dataclasses, so Litestar or Django integrations can follow the same pattern.
Compatibility with better-auth (TypeScript)
The wire protocol and storage format follow the TypeScript implementation closely. A Python service can share a database with a TypeScript better-auth app:
| Routes and JSON shapes | Same paths (/sign-in/email, /get-session, /callback/{provider}, ...), same success and error bodies, same codes (USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL 422, INVALID_EMAIL_OR_PASSWORD 401, ...) |
| Database schema | Identical user / session / account / verification tables, camelCase columns |
| Password hashes | Exact scrypt format (N=16384, r=16, p=1, dkLen=64, NFKC, hex salt:key). Passwords created by the TypeScript library verify in Python, and vice versa. |
| Session cookies | Same name (better-auth.session_token, __Secure- over HTTPS) and signing scheme (HMAC-SHA256, base64, URI-encoded token.sig) |
| IDs and tokens | Same alphabets and lengths (62-character IDs, 64-character state and verification tokens) |
Known divergences in v0.1: email-verification and reset tokens are stored in the database (the TypeScript library signs verify-email tokens as JWTs), bearer auth is built into the core (a plugin over there), and cookie cache plus secondary storage are not implemented yet.
Install
uv add better-auth-server[fastapi,sqlalchemy]
# or: pip install "better-auth-server[fastapi,sqlalchemy]"
The core has a single dependency, httpx. The fastapi and sqlalchemy extras pull in the rest.
Quickstart
Run the included demo:
uv run uvicorn examples.fastapi_app:app --reload
# health
curl -s localhost:8000/api/auth/ok
# sign up (sets a session cookie)
curl -s -c /tmp/jar -X POST localhost:8000/api/auth/sign-up/email \
-H 'content-type: application/json' \
-d '{"name": "Ada", "email": "ada@example.com", "password": "s3cret-password"}'
# who am I?
curl -s -b /tmp/jar localhost:8000/api/auth/get-session
curl -s -b /tmp/jar localhost:8000/me
# sign out
curl -s -b /tmp/jar -c /tmp/jar -X POST localhost:8000/api/auth/sign-out
API clients can skip cookies entirely and send Authorization: Bearer <token> with the token returned by sign-in or sign-up.
Configuration
from better_auth import (
BetterAuth, EmailAndPassword, EmailVerification, SessionOptions, RateLimit, GitHub, Google,
)
async def send_reset(user, url, token): ... # plug your mailer
async def send_verification(user, url, token): ...
auth = BetterAuth(
secret=os.environ["BETTER_AUTH_SECRET"], # >= 32 chars, required
base_url="https://example.com", # cookies become Secure/__Secure- on https
base_path="/api/auth", # default
adapter=SQLAlchemyAdapter(engine), # default: MemoryAdapter() (dev only!)
email_and_password=EmailAndPassword(
enabled=True,
min_password_length=8,
require_email_verification=False,
auto_sign_in=True,
send_reset_password=send_reset,
revoke_sessions_on_password_reset=False,
),
email_verification=EmailVerification(
send_verification_email=send_verification,
send_on_sign_up=False,
auto_sign_in_after_verification=False,
),
social_providers={
"github": GitHub(client_id="...", client_secret="..."),
"google": Google(client_id="...", client_secret="..."),
},
session=SessionOptions(expires_in=7 * 86400, update_age=86400),
rate_limit=RateLimit(enabled=True), # better-auth path rules built in
trusted_origins=["https://app.example.com"], # extra origins for CSRF + redirects
plugins=[...],
hooks={"user_created_before": ..., "user_created_after": ...},
)
Database
Tables follow better-auth's core schema (user, session, account, verification).
from sqlalchemy.ext.asyncio import create_async_engine
from better_auth.adapters.sqlalchemy import SQLAlchemyAdapter
engine = create_async_engine("postgresql+asyncpg://...") # or sqlite+aiosqlite, mysql+aiomysql
adapter = SQLAlchemyAdapter(engine)
auth = BetterAuth(secret=..., adapter=adapter, ...)
await adapter.create_tables() # dev convenience; use Alembic in production
A custom adapter implements five async methods over dict rows. See better_auth.adapters.base.BaseAdapter (create, find_one, find_many, update, delete_many).
Social providers
social_providers={"github": GitHub(client_id=..., client_secret=...)}
POST /api/auth/sign-in/social {"provider": "github", "callbackURL": "/dashboard"} returns {"url": ..., "redirect": true}. Send the browser to that URL; the callback sets the session cookie and redirects to callbackURL. A custom provider is one dataclass:
from better_auth import OAuthProvider
gitlab = OAuthProvider(
client_id=..., client_secret=..., provider_id="gitlab",
authorize_url="https://gitlab.com/oauth/authorize",
token_url="https://gitlab.com/oauth/token",
userinfo_url="https://gitlab.com/oauth/userinfo", # OIDC userinfo shape
scopes=["openid", "email", "profile"], use_pkce=True,
)
Override fetch_user() for providers whose user payload is not OIDC-shaped (see the GitHub and Discord sources).
Plugins
from better_auth import AuthResponse, Plugin
class ApiKeys(Plugin):
id = "api-keys"
schema = {"apikey": {...}} # extra tables, migrated like core ones
def routes(self):
return [("POST", "/api-keys/create", self.create)]
async def create(self, ctx):
result = await ctx.require_session()
...
return {"key": "..."}
async def before(self, ctx): # runs before every endpoint
return None # or AuthResponse(...) to short-circuit
Security notes
- Non-GET requests are origin-checked (CSRF) against
base_urlandtrusted_origins. - Every
callbackURLandredirectTois validated against trusted origins, which blocks open redirects. - Sign-in runs a dummy scrypt when the user does not exist, so unknown email and wrong password take the same time and return the same 401.
- Rate limiting is in-memory, per process. Behind a multi-worker or proxied setup, also rate-limit at the edge.
x-forwarded-foris honored for the client IP. MemoryAdapteris the default so quickstarts work. Switch to a real adapter for anything persistent.
Roadmap
Core: change-email, delete-user, link-social, refresh-token/get-access-token, cookie cache, secondary storage (Redis), CLI schema migrations. Plugins: two-factor, magic link, username, organization, admin, API keys, passkeys. Integrations: Litestar, Django, Flask.
Development
uv sync --all-extras
uv run pre-commit install
uv run pytest # e2e over ASGI, both adapters, mocked OAuth
uv run ruff check .
uv run ty check
See CONTRIBUTING.md for guidelines. Commits follow Conventional Commits.
License
MIT. Inspired by and API-compatible with better-auth, also MIT.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file better_auth_server-0.1.0.tar.gz.
File metadata
- Download URL: better_auth_server-0.1.0.tar.gz
- Upload date:
- Size: 25.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fecd66112f588e0fe6cbe01c9960e82b4320e17a3d0a3a10c6bf7843554b5a0f
|
|
| MD5 |
ebe1232f686fa7ebf10f0c05c2447da1
|
|
| BLAKE2b-256 |
1e31e0bc984e3a2021a4a3103f24ad70d6f628e7c29fae1257a395535f10c5c1
|
File details
Details for the file better_auth_server-0.1.0-py3-none-any.whl.
File metadata
- Download URL: better_auth_server-0.1.0-py3-none-any.whl
- Upload date:
- Size: 32.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f21bb4635c360a8b221843afcfdac159e6e9f19fdd98a63789e6e3b157ada229
|
|
| MD5 |
0efd744f8a603c39a6d4e68308cb2ca7
|
|
| BLAKE2b-256 |
cfe3d9f2174434f550be46d40ee927082154c3d900b70f6cd694a066416cb3c1
|