better-auth-server 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oumar Barry and better-auth-py contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,247 @@
1
+ Metadata-Version: 2.4
2
+ Name: better-auth-server
3
+ Version: 0.1.0
4
+ Summary: Framework-agnostic authentication for Python, ported from better-auth, with a FastAPI integration.
5
+ Keywords: auth,authentication,oauth,oauth2,session,fastapi,better-auth
6
+ Author: Oumar Barry
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: Session
18
+ Classifier: Topic :: Security
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: fastapi>=0.110 ; extra == 'fastapi'
22
+ Requires-Dist: sqlalchemy[asyncio]>=2.0 ; extra == 'sqlalchemy'
23
+ Requires-Python: >=3.10
24
+ Project-URL: Homepage, https://github.com/oumarbarry/better-auth-py
25
+ Project-URL: Repository, https://github.com/oumarbarry/better-auth-py
26
+ Project-URL: Issues, https://github.com/oumarbarry/better-auth-py/issues
27
+ Project-URL: Changelog, https://github.com/oumarbarry/better-auth-py/blob/main/CHANGELOG.md
28
+ Provides-Extra: fastapi
29
+ Provides-Extra: sqlalchemy
30
+ Description-Content-Type: text/markdown
31
+
32
+ # better-auth-server
33
+
34
+ [![CI](https://github.com/oumarbarry/better-auth-py/actions/workflows/ci.yml/badge.svg)](https://github.com/oumarbarry/better-auth-py/actions/workflows/ci.yml)
35
+
36
+ **Authentication for Python, ported from [better-auth](https://better-auth.com). Ships with a FastAPI integration.**
37
+
38
+ 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.
39
+
40
+ ```python
41
+ from better_auth import BetterAuth, EmailAndPassword
42
+ from better_auth.integrations.fastapi import BetterAuthFastAPI
43
+ from fastapi import Depends, FastAPI
44
+
45
+ auth = BetterAuth(
46
+ secret="...", # openssl rand -base64 32
47
+ base_url="http://localhost:8000",
48
+ email_and_password=EmailAndPassword(enabled=True),
49
+ )
50
+
51
+ app = FastAPI()
52
+ ba = BetterAuthFastAPI(auth)
53
+ app.include_router(ba.router) # mounts /api/auth/*
54
+
55
+ @app.get("/me")
56
+ async def me(result: dict = Depends(ba.require_session)):
57
+ return result["user"]
58
+ ```
59
+
60
+ 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.
61
+
62
+ ## Features
63
+
64
+ - Email and password: sign-up, sign-in, change/set/verify password, reset flow, email verification.
65
+ - 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.
66
+ - Sessions in your database: HMAC-signed cookies, sliding expiry (`expires_in`/`update_age`), `rememberMe`, list and revoke endpoints, bearer tokens for API clients.
67
+ - 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.
68
+ - Plugins can add routes, extend the database schema, and hook before and after every request.
69
+ - 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.
70
+ - 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.
71
+
72
+ ## Compatibility with better-auth (TypeScript)
73
+
74
+ The wire protocol and storage format follow the TypeScript implementation closely. A Python service can share a database with a TypeScript better-auth app:
75
+
76
+ | | |
77
+ |---|---|
78
+ | 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, ...) |
79
+ | Database schema | Identical `user` / `session` / `account` / `verification` tables, camelCase columns |
80
+ | 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. |
81
+ | Session cookies | Same name (`better-auth.session_token`, `__Secure-` over HTTPS) and signing scheme (HMAC-SHA256, base64, URI-encoded `token.sig`) |
82
+ | IDs and tokens | Same alphabets and lengths (62-character IDs, 64-character state and verification tokens) |
83
+
84
+ 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.
85
+
86
+ ## Install
87
+
88
+ ```bash
89
+ uv add better-auth-server[fastapi,sqlalchemy]
90
+ # or: pip install "better-auth-server[fastapi,sqlalchemy]"
91
+ ```
92
+
93
+ The core has a single dependency, `httpx`. The `fastapi` and `sqlalchemy` extras pull in the rest.
94
+
95
+ ## Quickstart
96
+
97
+ Run the included demo:
98
+
99
+ ```bash
100
+ uv run uvicorn examples.fastapi_app:app --reload
101
+ ```
102
+
103
+ ```bash
104
+ # health
105
+ curl -s localhost:8000/api/auth/ok
106
+
107
+ # sign up (sets a session cookie)
108
+ curl -s -c /tmp/jar -X POST localhost:8000/api/auth/sign-up/email \
109
+ -H 'content-type: application/json' \
110
+ -d '{"name": "Ada", "email": "ada@example.com", "password": "s3cret-password"}'
111
+
112
+ # who am I?
113
+ curl -s -b /tmp/jar localhost:8000/api/auth/get-session
114
+ curl -s -b /tmp/jar localhost:8000/me
115
+
116
+ # sign out
117
+ curl -s -b /tmp/jar -c /tmp/jar -X POST localhost:8000/api/auth/sign-out
118
+ ```
119
+
120
+ API clients can skip cookies entirely and send `Authorization: Bearer <token>` with the `token` returned by sign-in or sign-up.
121
+
122
+ ## Configuration
123
+
124
+ ```python
125
+ from better_auth import (
126
+ BetterAuth, EmailAndPassword, EmailVerification, SessionOptions, RateLimit, GitHub, Google,
127
+ )
128
+
129
+ async def send_reset(user, url, token): ... # plug your mailer
130
+ async def send_verification(user, url, token): ...
131
+
132
+ auth = BetterAuth(
133
+ secret=os.environ["BETTER_AUTH_SECRET"], # >= 32 chars, required
134
+ base_url="https://example.com", # cookies become Secure/__Secure- on https
135
+ base_path="/api/auth", # default
136
+ adapter=SQLAlchemyAdapter(engine), # default: MemoryAdapter() (dev only!)
137
+ email_and_password=EmailAndPassword(
138
+ enabled=True,
139
+ min_password_length=8,
140
+ require_email_verification=False,
141
+ auto_sign_in=True,
142
+ send_reset_password=send_reset,
143
+ revoke_sessions_on_password_reset=False,
144
+ ),
145
+ email_verification=EmailVerification(
146
+ send_verification_email=send_verification,
147
+ send_on_sign_up=False,
148
+ auto_sign_in_after_verification=False,
149
+ ),
150
+ social_providers={
151
+ "github": GitHub(client_id="...", client_secret="..."),
152
+ "google": Google(client_id="...", client_secret="..."),
153
+ },
154
+ session=SessionOptions(expires_in=7 * 86400, update_age=86400),
155
+ rate_limit=RateLimit(enabled=True), # better-auth path rules built in
156
+ trusted_origins=["https://app.example.com"], # extra origins for CSRF + redirects
157
+ plugins=[...],
158
+ hooks={"user_created_before": ..., "user_created_after": ...},
159
+ )
160
+ ```
161
+
162
+ ## Database
163
+
164
+ Tables follow better-auth's core schema (`user`, `session`, `account`, `verification`).
165
+
166
+ ```python
167
+ from sqlalchemy.ext.asyncio import create_async_engine
168
+ from better_auth.adapters.sqlalchemy import SQLAlchemyAdapter
169
+
170
+ engine = create_async_engine("postgresql+asyncpg://...") # or sqlite+aiosqlite, mysql+aiomysql
171
+ adapter = SQLAlchemyAdapter(engine)
172
+ auth = BetterAuth(secret=..., adapter=adapter, ...)
173
+ await adapter.create_tables() # dev convenience; use Alembic in production
174
+ ```
175
+
176
+ A custom adapter implements five async methods over dict rows. See `better_auth.adapters.base.BaseAdapter` (`create`, `find_one`, `find_many`, `update`, `delete_many`).
177
+
178
+ ## Social providers
179
+
180
+ ```python
181
+ social_providers={"github": GitHub(client_id=..., client_secret=...)}
182
+ ```
183
+
184
+ `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:
185
+
186
+ ```python
187
+ from better_auth import OAuthProvider
188
+
189
+ gitlab = OAuthProvider(
190
+ client_id=..., client_secret=..., provider_id="gitlab",
191
+ authorize_url="https://gitlab.com/oauth/authorize",
192
+ token_url="https://gitlab.com/oauth/token",
193
+ userinfo_url="https://gitlab.com/oauth/userinfo", # OIDC userinfo shape
194
+ scopes=["openid", "email", "profile"], use_pkce=True,
195
+ )
196
+ ```
197
+
198
+ Override `fetch_user()` for providers whose user payload is not OIDC-shaped (see the GitHub and Discord sources).
199
+
200
+ ## Plugins
201
+
202
+ ```python
203
+ from better_auth import AuthResponse, Plugin
204
+
205
+ class ApiKeys(Plugin):
206
+ id = "api-keys"
207
+ schema = {"apikey": {...}} # extra tables, migrated like core ones
208
+
209
+ def routes(self):
210
+ return [("POST", "/api-keys/create", self.create)]
211
+
212
+ async def create(self, ctx):
213
+ result = await ctx.require_session()
214
+ ...
215
+ return {"key": "..."}
216
+
217
+ async def before(self, ctx): # runs before every endpoint
218
+ return None # or AuthResponse(...) to short-circuit
219
+ ```
220
+
221
+ ## Security notes
222
+
223
+ - Non-GET requests are origin-checked (CSRF) against `base_url` and `trusted_origins`.
224
+ - Every `callbackURL` and `redirectTo` is validated against trusted origins, which blocks open redirects.
225
+ - 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.
226
+ - Rate limiting is in-memory, per process. Behind a multi-worker or proxied setup, also rate-limit at the edge. `x-forwarded-for` is honored for the client IP.
227
+ - `MemoryAdapter` is the default so quickstarts work. Switch to a real adapter for anything persistent.
228
+
229
+ ## Roadmap
230
+
231
+ 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.
232
+
233
+ ## Development
234
+
235
+ ```bash
236
+ uv sync --all-extras
237
+ uv run pre-commit install
238
+ uv run pytest # e2e over ASGI, both adapters, mocked OAuth
239
+ uv run ruff check .
240
+ uv run ty check
241
+ ```
242
+
243
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Commits follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
244
+
245
+ ## License
246
+
247
+ [MIT](LICENSE). Inspired by and API-compatible with [better-auth](https://github.com/better-auth/better-auth), also MIT.
@@ -0,0 +1,216 @@
1
+ # better-auth-server
2
+
3
+ [![CI](https://github.com/oumarbarry/better-auth-py/actions/workflows/ci.yml/badge.svg)](https://github.com/oumarbarry/better-auth-py/actions/workflows/ci.yml)
4
+
5
+ **Authentication for Python, ported from [better-auth](https://better-auth.com). Ships with a FastAPI integration.**
6
+
7
+ 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.
8
+
9
+ ```python
10
+ from better_auth import BetterAuth, EmailAndPassword
11
+ from better_auth.integrations.fastapi import BetterAuthFastAPI
12
+ from fastapi import Depends, FastAPI
13
+
14
+ auth = BetterAuth(
15
+ secret="...", # openssl rand -base64 32
16
+ base_url="http://localhost:8000",
17
+ email_and_password=EmailAndPassword(enabled=True),
18
+ )
19
+
20
+ app = FastAPI()
21
+ ba = BetterAuthFastAPI(auth)
22
+ app.include_router(ba.router) # mounts /api/auth/*
23
+
24
+ @app.get("/me")
25
+ async def me(result: dict = Depends(ba.require_session)):
26
+ return result["user"]
27
+ ```
28
+
29
+ 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.
30
+
31
+ ## Features
32
+
33
+ - Email and password: sign-up, sign-in, change/set/verify password, reset flow, email verification.
34
+ - 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.
35
+ - Sessions in your database: HMAC-signed cookies, sliding expiry (`expires_in`/`update_age`), `rememberMe`, list and revoke endpoints, bearer tokens for API clients.
36
+ - 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.
37
+ - Plugins can add routes, extend the database schema, and hook before and after every request.
38
+ - 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.
39
+ - 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.
40
+
41
+ ## Compatibility with better-auth (TypeScript)
42
+
43
+ The wire protocol and storage format follow the TypeScript implementation closely. A Python service can share a database with a TypeScript better-auth app:
44
+
45
+ | | |
46
+ |---|---|
47
+ | 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, ...) |
48
+ | Database schema | Identical `user` / `session` / `account` / `verification` tables, camelCase columns |
49
+ | 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. |
50
+ | Session cookies | Same name (`better-auth.session_token`, `__Secure-` over HTTPS) and signing scheme (HMAC-SHA256, base64, URI-encoded `token.sig`) |
51
+ | IDs and tokens | Same alphabets and lengths (62-character IDs, 64-character state and verification tokens) |
52
+
53
+ 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.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ uv add better-auth-server[fastapi,sqlalchemy]
59
+ # or: pip install "better-auth-server[fastapi,sqlalchemy]"
60
+ ```
61
+
62
+ The core has a single dependency, `httpx`. The `fastapi` and `sqlalchemy` extras pull in the rest.
63
+
64
+ ## Quickstart
65
+
66
+ Run the included demo:
67
+
68
+ ```bash
69
+ uv run uvicorn examples.fastapi_app:app --reload
70
+ ```
71
+
72
+ ```bash
73
+ # health
74
+ curl -s localhost:8000/api/auth/ok
75
+
76
+ # sign up (sets a session cookie)
77
+ curl -s -c /tmp/jar -X POST localhost:8000/api/auth/sign-up/email \
78
+ -H 'content-type: application/json' \
79
+ -d '{"name": "Ada", "email": "ada@example.com", "password": "s3cret-password"}'
80
+
81
+ # who am I?
82
+ curl -s -b /tmp/jar localhost:8000/api/auth/get-session
83
+ curl -s -b /tmp/jar localhost:8000/me
84
+
85
+ # sign out
86
+ curl -s -b /tmp/jar -c /tmp/jar -X POST localhost:8000/api/auth/sign-out
87
+ ```
88
+
89
+ API clients can skip cookies entirely and send `Authorization: Bearer <token>` with the `token` returned by sign-in or sign-up.
90
+
91
+ ## Configuration
92
+
93
+ ```python
94
+ from better_auth import (
95
+ BetterAuth, EmailAndPassword, EmailVerification, SessionOptions, RateLimit, GitHub, Google,
96
+ )
97
+
98
+ async def send_reset(user, url, token): ... # plug your mailer
99
+ async def send_verification(user, url, token): ...
100
+
101
+ auth = BetterAuth(
102
+ secret=os.environ["BETTER_AUTH_SECRET"], # >= 32 chars, required
103
+ base_url="https://example.com", # cookies become Secure/__Secure- on https
104
+ base_path="/api/auth", # default
105
+ adapter=SQLAlchemyAdapter(engine), # default: MemoryAdapter() (dev only!)
106
+ email_and_password=EmailAndPassword(
107
+ enabled=True,
108
+ min_password_length=8,
109
+ require_email_verification=False,
110
+ auto_sign_in=True,
111
+ send_reset_password=send_reset,
112
+ revoke_sessions_on_password_reset=False,
113
+ ),
114
+ email_verification=EmailVerification(
115
+ send_verification_email=send_verification,
116
+ send_on_sign_up=False,
117
+ auto_sign_in_after_verification=False,
118
+ ),
119
+ social_providers={
120
+ "github": GitHub(client_id="...", client_secret="..."),
121
+ "google": Google(client_id="...", client_secret="..."),
122
+ },
123
+ session=SessionOptions(expires_in=7 * 86400, update_age=86400),
124
+ rate_limit=RateLimit(enabled=True), # better-auth path rules built in
125
+ trusted_origins=["https://app.example.com"], # extra origins for CSRF + redirects
126
+ plugins=[...],
127
+ hooks={"user_created_before": ..., "user_created_after": ...},
128
+ )
129
+ ```
130
+
131
+ ## Database
132
+
133
+ Tables follow better-auth's core schema (`user`, `session`, `account`, `verification`).
134
+
135
+ ```python
136
+ from sqlalchemy.ext.asyncio import create_async_engine
137
+ from better_auth.adapters.sqlalchemy import SQLAlchemyAdapter
138
+
139
+ engine = create_async_engine("postgresql+asyncpg://...") # or sqlite+aiosqlite, mysql+aiomysql
140
+ adapter = SQLAlchemyAdapter(engine)
141
+ auth = BetterAuth(secret=..., adapter=adapter, ...)
142
+ await adapter.create_tables() # dev convenience; use Alembic in production
143
+ ```
144
+
145
+ A custom adapter implements five async methods over dict rows. See `better_auth.adapters.base.BaseAdapter` (`create`, `find_one`, `find_many`, `update`, `delete_many`).
146
+
147
+ ## Social providers
148
+
149
+ ```python
150
+ social_providers={"github": GitHub(client_id=..., client_secret=...)}
151
+ ```
152
+
153
+ `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:
154
+
155
+ ```python
156
+ from better_auth import OAuthProvider
157
+
158
+ gitlab = OAuthProvider(
159
+ client_id=..., client_secret=..., provider_id="gitlab",
160
+ authorize_url="https://gitlab.com/oauth/authorize",
161
+ token_url="https://gitlab.com/oauth/token",
162
+ userinfo_url="https://gitlab.com/oauth/userinfo", # OIDC userinfo shape
163
+ scopes=["openid", "email", "profile"], use_pkce=True,
164
+ )
165
+ ```
166
+
167
+ Override `fetch_user()` for providers whose user payload is not OIDC-shaped (see the GitHub and Discord sources).
168
+
169
+ ## Plugins
170
+
171
+ ```python
172
+ from better_auth import AuthResponse, Plugin
173
+
174
+ class ApiKeys(Plugin):
175
+ id = "api-keys"
176
+ schema = {"apikey": {...}} # extra tables, migrated like core ones
177
+
178
+ def routes(self):
179
+ return [("POST", "/api-keys/create", self.create)]
180
+
181
+ async def create(self, ctx):
182
+ result = await ctx.require_session()
183
+ ...
184
+ return {"key": "..."}
185
+
186
+ async def before(self, ctx): # runs before every endpoint
187
+ return None # or AuthResponse(...) to short-circuit
188
+ ```
189
+
190
+ ## Security notes
191
+
192
+ - Non-GET requests are origin-checked (CSRF) against `base_url` and `trusted_origins`.
193
+ - Every `callbackURL` and `redirectTo` is validated against trusted origins, which blocks open redirects.
194
+ - 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.
195
+ - Rate limiting is in-memory, per process. Behind a multi-worker or proxied setup, also rate-limit at the edge. `x-forwarded-for` is honored for the client IP.
196
+ - `MemoryAdapter` is the default so quickstarts work. Switch to a real adapter for anything persistent.
197
+
198
+ ## Roadmap
199
+
200
+ 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.
201
+
202
+ ## Development
203
+
204
+ ```bash
205
+ uv sync --all-extras
206
+ uv run pre-commit install
207
+ uv run pytest # e2e over ASGI, both adapters, mocked OAuth
208
+ uv run ruff check .
209
+ uv run ty check
210
+ ```
211
+
212
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Commits follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
213
+
214
+ ## License
215
+
216
+ [MIT](LICENSE). Inspired by and API-compatible with [better-auth](https://github.com/better-auth/better-auth), also MIT.
@@ -0,0 +1,95 @@
1
+ [project]
2
+ name = "better-auth-server"
3
+ version = "0.1.0"
4
+ description = "Framework-agnostic authentication for Python, ported from better-auth, with a FastAPI integration."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.10"
9
+ keywords = [
10
+ "auth",
11
+ "authentication",
12
+ "oauth",
13
+ "oauth2",
14
+ "session",
15
+ "fastapi",
16
+ "better-auth",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Intended Audience :: Developers",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Internet :: WWW/HTTP :: Session",
28
+ "Topic :: Security",
29
+ "Typing :: Typed",
30
+ ]
31
+ dependencies = ["httpx>=0.27"]
32
+
33
+ [[project.authors]]
34
+ name = "Oumar Barry"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/oumarbarry/better-auth-py"
38
+ Repository = "https://github.com/oumarbarry/better-auth-py"
39
+ Issues = "https://github.com/oumarbarry/better-auth-py/issues"
40
+ Changelog = "https://github.com/oumarbarry/better-auth-py/blob/main/CHANGELOG.md"
41
+
42
+ [project.optional-dependencies]
43
+ fastapi = ["fastapi>=0.110"]
44
+ sqlalchemy = ["sqlalchemy[asyncio]>=2.0"]
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "pytest>=8",
49
+ "pytest-asyncio>=0.24",
50
+ "ruff>=0.8",
51
+ "fastapi>=0.110",
52
+ "sqlalchemy[asyncio]>=2.0",
53
+ "aiosqlite>=0.20",
54
+ "uvicorn>=0.30",
55
+ "ty>=0.0.57",
56
+ "pre-commit>=4.6.0",
57
+ ]
58
+
59
+ [build-system]
60
+ requires = ["uv_build>=0.11.23,<0.12.0"]
61
+ build-backend = "uv_build"
62
+
63
+ [tool.uv.build-backend]
64
+ module-name = "better_auth"
65
+
66
+ [tool.pytest.ini_options]
67
+ asyncio_mode = "auto"
68
+ asyncio_default_fixture_loop_scope = "function"
69
+ testpaths = ["tests"]
70
+
71
+ [tool.ruff]
72
+ line-length = 100
73
+ src = [
74
+ "src",
75
+ "tests",
76
+ ]
77
+
78
+ [tool.ruff.lint]
79
+ select = [
80
+ "E",
81
+ "F",
82
+ "W",
83
+ "I",
84
+ "UP",
85
+ "B",
86
+ "SIM",
87
+ "RUF",
88
+ ]
89
+
90
+ [tool.ruff.lint.per-file-ignores]
91
+ "tests/*" = [
92
+ "B008",
93
+ "RUF012",
94
+ ]
95
+ "examples/*" = ["B008"]
@@ -0,0 +1,75 @@
1
+ [project]
2
+ name = "better-auth-server"
3
+ version = "0.1.0"
4
+ description = "Framework-agnostic authentication for Python, ported from better-auth, with a FastAPI integration."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Oumar Barry" }
8
+ ]
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.10"
12
+ keywords = ["auth", "authentication", "oauth", "oauth2", "session", "fastapi", "better-auth"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Topic :: Internet :: WWW/HTTP :: Session",
23
+ "Topic :: Security",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "httpx>=0.27",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/oumarbarry/better-auth-py"
32
+ Repository = "https://github.com/oumarbarry/better-auth-py"
33
+ Issues = "https://github.com/oumarbarry/better-auth-py/issues"
34
+ Changelog = "https://github.com/oumarbarry/better-auth-py/blob/main/CHANGELOG.md"
35
+
36
+ [project.optional-dependencies]
37
+ fastapi = ["fastapi>=0.110"]
38
+ sqlalchemy = ["sqlalchemy[asyncio]>=2.0"]
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=8",
43
+ "pytest-asyncio>=0.24",
44
+ "ruff>=0.8",
45
+ "fastapi>=0.110",
46
+ "sqlalchemy[asyncio]>=2.0",
47
+ "aiosqlite>=0.20",
48
+ "uvicorn>=0.30",
49
+ "ty>=0.0.57",
50
+ "pre-commit>=4.6.0",
51
+ ]
52
+
53
+ [build-system]
54
+ requires = ["uv_build>=0.11.23,<0.12.0"]
55
+ build-backend = "uv_build"
56
+
57
+ [tool.uv.build-backend]
58
+ module-name = "better_auth"
59
+
60
+ [tool.pytest.ini_options]
61
+ asyncio_mode = "auto"
62
+ asyncio_default_fixture_loop_scope = "function"
63
+ testpaths = ["tests"]
64
+
65
+ [tool.ruff]
66
+ line-length = 100
67
+ src = ["src", "tests"]
68
+
69
+ [tool.ruff.lint]
70
+ select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
71
+
72
+ [tool.ruff.lint.per-file-ignores]
73
+ # B008: `Depends(...)` in defaults is the FastAPI idiom; RUF012: plugin schema class attrs
74
+ "tests/*" = ["B008", "RUF012"]
75
+ "examples/*" = ["B008"]