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.
- better_auth_server-0.1.0/LICENSE +21 -0
- better_auth_server-0.1.0/PKG-INFO +247 -0
- better_auth_server-0.1.0/README.md +216 -0
- better_auth_server-0.1.0/pyproject.toml +95 -0
- better_auth_server-0.1.0/pyproject.toml.orig +75 -0
- better_auth_server-0.1.0/src/better_auth/__init__.py +40 -0
- better_auth_server-0.1.0/src/better_auth/adapters/__init__.py +4 -0
- better_auth_server-0.1.0/src/better_auth/adapters/base.py +44 -0
- better_auth_server-0.1.0/src/better_auth/adapters/memory.py +55 -0
- better_auth_server-0.1.0/src/better_auth/adapters/sqlalchemy.py +166 -0
- better_auth_server-0.1.0/src/better_auth/auth.py +239 -0
- better_auth_server-0.1.0/src/better_auth/config.py +55 -0
- better_auth_server-0.1.0/src/better_auth/crypto.py +91 -0
- better_auth_server-0.1.0/src/better_auth/endpoints.py +582 -0
- better_auth_server-0.1.0/src/better_auth/integrations/__init__.py +0 -0
- better_auth_server-0.1.0/src/better_auth/integrations/fastapi.py +85 -0
- better_auth_server-0.1.0/src/better_auth/oauth.py +384 -0
- better_auth_server-0.1.0/src/better_auth/plugins.py +31 -0
- better_auth_server-0.1.0/src/better_auth/py.typed +0 -0
- better_auth_server-0.1.0/src/better_auth/schema.py +75 -0
- better_auth_server-0.1.0/src/better_auth/session.py +141 -0
- better_auth_server-0.1.0/src/better_auth/types.py +112 -0
|
@@ -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
|
+
[](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
|
+
[](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"]
|