belgie 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.
- belgie-0.1.0/PKG-INFO +231 -0
- belgie-0.1.0/README.md +204 -0
- belgie-0.1.0/pyproject.toml +138 -0
- belgie-0.1.0/src/belgie/.DS_Store +0 -0
- belgie-0.1.0/src/belgie/__init__.py +97 -0
- belgie-0.1.0/src/belgie/alchemy.py +24 -0
- belgie-0.1.0/src/belgie/auth/__init__.py +65 -0
- belgie-0.1.0/src/belgie/auth/core/__init__.py +0 -0
- belgie-0.1.0/src/belgie/auth/core/auth.py +368 -0
- belgie-0.1.0/src/belgie/auth/core/client.py +204 -0
- belgie-0.1.0/src/belgie/auth/core/exceptions.py +26 -0
- belgie-0.1.0/src/belgie/auth/core/hooks.py +87 -0
- belgie-0.1.0/src/belgie/auth/core/settings.py +100 -0
- belgie-0.1.0/src/belgie/auth/providers/__init__.py +10 -0
- belgie-0.1.0/src/belgie/auth/providers/google.py +284 -0
- belgie-0.1.0/src/belgie/auth/providers/protocols.py +43 -0
- belgie-0.1.0/src/belgie/auth/py.typed +0 -0
- belgie-0.1.0/src/belgie/auth/session/__init__.py +3 -0
- belgie-0.1.0/src/belgie/auth/session/manager.py +168 -0
- belgie-0.1.0/src/belgie/auth/utils/__init__.py +9 -0
- belgie-0.1.0/src/belgie/auth/utils/crypto.py +10 -0
- belgie-0.1.0/src/belgie/auth/utils/scopes.py +49 -0
- belgie-0.1.0/src/belgie/mcp.py +12 -0
- belgie-0.1.0/src/belgie/oauth.py +12 -0
- belgie-0.1.0/src/belgie/proto.py +22 -0
belgie-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: belgie
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Modern authentication for FastAPI
|
|
5
|
+
Author: Matt LeMay
|
|
6
|
+
Author-email: Matt LeMay <mplemay@users.noreply.github.com>
|
|
7
|
+
Requires-Dist: belgie-proto
|
|
8
|
+
Requires-Dist: fastapi>=0.100
|
|
9
|
+
Requires-Dist: httpx>=0.24
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
12
|
+
Requires-Dist: python-multipart>=0.0.20
|
|
13
|
+
Requires-Dist: belgie-alchemy ; extra == 'alchemy'
|
|
14
|
+
Requires-Dist: belgie-alchemy ; extra == 'all'
|
|
15
|
+
Requires-Dist: belgie-mcp ; extra == 'all'
|
|
16
|
+
Requires-Dist: belgie-oauth ; extra == 'all'
|
|
17
|
+
Requires-Dist: uvicorn[standard]>=0.38.0 ; extra == 'examples'
|
|
18
|
+
Requires-Dist: belgie-mcp ; extra == 'mcp'
|
|
19
|
+
Requires-Dist: belgie-oauth ; extra == 'oauth'
|
|
20
|
+
Requires-Python: >=3.12, <3.15
|
|
21
|
+
Provides-Extra: alchemy
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Provides-Extra: examples
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Provides-Extra: oauth
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Belgie
|
|
29
|
+
|
|
30
|
+
Self-hosted, type-safe authentication for FastAPI that makes Google OAuth and secure session cookies work with almost
|
|
31
|
+
zero glue code. Keep your data, skip per-user SaaS bills, and still get a polished developer experience.
|
|
32
|
+
|
|
33
|
+
## Who this is for
|
|
34
|
+
|
|
35
|
+
- FastAPI teams that want Google sign-in and protected routes today, not after weeks of wiring.
|
|
36
|
+
- Product engineers who prefer first-class type hints and adapter-driven design over magic.
|
|
37
|
+
- Startups that would rather own their user data and avoid per-MAU pricing from hosted identity vendors.
|
|
38
|
+
|
|
39
|
+
## What it solves
|
|
40
|
+
|
|
41
|
+
- End-to-end Google OAuth 2.0 flow with CSRF-safe state storage.
|
|
42
|
+
- Sliding-window, signed session cookies (no JWT juggling required).
|
|
43
|
+
- Drop-in FastAPI dependencies for `auth.user`, `auth.session`, and scoped access.
|
|
44
|
+
- A thin SQLAlchemy adapter that works with your existing models.
|
|
45
|
+
- Hooks so you can plug in logging, analytics, or audit trails without forking.
|
|
46
|
+
|
|
47
|
+
## How it compares
|
|
48
|
+
|
|
49
|
+
- **fastapi-users**: feature-rich but now in maintenance mode and optimized for password-plus-OAuth flows. Belgie
|
|
50
|
+
focuses on OAuth + session UX, keeps the surface area small, and ships type-driven adapters out of the box.
|
|
51
|
+
- **Hosted identity (Auth0, Clerk, Supabase Auth)**: great UIs and more providers, but billed per Monthly Active User
|
|
52
|
+
and hosted off your stack. Belgie is MIT-licensed, runs in your app, and never charges per user.
|
|
53
|
+
|
|
54
|
+
## Features at a glance
|
|
55
|
+
|
|
56
|
+
- Google OAuth provider with ready-made router (`/auth/signin/google`, `/auth/callback/google`, `/auth/signout`).
|
|
57
|
+
- Session manager with sliding expiry and secure cookie defaults (HttpOnly, SameSite, Secure).
|
|
58
|
+
- Scope-aware dependency for route protection (`Security(auth.user, scopes=[...])`).
|
|
59
|
+
- Modern Python (3.12+), full typing, and protocol-based models.
|
|
60
|
+
- Event hooks and utility helpers for custom workflows.
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install belgie
|
|
66
|
+
# or with uv
|
|
67
|
+
uv add belgie
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For SQLAlchemy adapter support:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install belgie[alchemy]
|
|
74
|
+
# or with uv
|
|
75
|
+
uv add belgie[alchemy]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Optional extras: `belgie[mcp]`, `belgie[oauth]`, or `belgie[all]`.
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
### 1) Define models
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from datetime import UTC, datetime
|
|
86
|
+
from uuid import UUID, uuid4
|
|
87
|
+
from sqlalchemy import ForeignKey, String
|
|
88
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Base(DeclarativeBase):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class User(Base):
|
|
96
|
+
__tablename__ = "users"
|
|
97
|
+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
|
98
|
+
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
|
99
|
+
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
100
|
+
image: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
101
|
+
email_verified: Mapped[bool] = mapped_column(default=False)
|
|
102
|
+
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
|
|
103
|
+
updated_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Account(Base):
|
|
107
|
+
__tablename__ = "accounts"
|
|
108
|
+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
|
109
|
+
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
|
110
|
+
provider: Mapped[str] = mapped_column(String(50))
|
|
111
|
+
provider_account_id: Mapped[str] = mapped_column(String(255))
|
|
112
|
+
access_token: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
|
113
|
+
refresh_token: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
|
114
|
+
expires_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
|
115
|
+
scope: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
116
|
+
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Session(Base):
|
|
120
|
+
__tablename__ = "sessions"
|
|
121
|
+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
|
122
|
+
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
|
123
|
+
expires_at: Mapped[datetime] = mapped_column(index=True)
|
|
124
|
+
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class OAuthState(Base):
|
|
128
|
+
__tablename__ = "oauth_states"
|
|
129
|
+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
|
130
|
+
state: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
|
131
|
+
expires_at: Mapped[datetime] = mapped_column(index=True)
|
|
132
|
+
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 2) Configure Belgie
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from belgie.auth import Auth, AuthSettings, GoogleProviderSettings
|
|
139
|
+
from belgie_alchemy import AlchemyAdapter
|
|
140
|
+
|
|
141
|
+
settings = AuthSettings(
|
|
142
|
+
secret="your-secret-key",
|
|
143
|
+
base_url="http://localhost:8000",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
adapter = AlchemyAdapter(
|
|
147
|
+
user=User,
|
|
148
|
+
account=Account,
|
|
149
|
+
session=Session,
|
|
150
|
+
oauth_state=OAuthState,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
auth = Auth(
|
|
154
|
+
settings=settings,
|
|
155
|
+
adapter=adapter,
|
|
156
|
+
providers={
|
|
157
|
+
"google": GoogleProviderSettings(
|
|
158
|
+
client_id="your-google-client-id",
|
|
159
|
+
client_secret="your-google-client-secret",
|
|
160
|
+
redirect_uri="http://localhost:8000/auth/provider/google/callback",
|
|
161
|
+
scopes=["openid", "email", "profile"],
|
|
162
|
+
),
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 3) Add routes to FastAPI
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from fastapi import Depends, FastAPI, Security
|
|
171
|
+
|
|
172
|
+
app = FastAPI()
|
|
173
|
+
app.include_router(auth.router)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.get("/")
|
|
177
|
+
async def home():
|
|
178
|
+
return {"message": "Welcome! Visit /auth/provider/google/signin to sign in"}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@app.get("/protected")
|
|
182
|
+
async def protected(user: User = Depends(auth.user)):
|
|
183
|
+
return {"email": user.email}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.get("/profile")
|
|
187
|
+
async def profile(user: User = Security(auth.user, scopes=["profile"])):
|
|
188
|
+
return {"name": user.name, "email": user.email}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Run it:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
uvicorn main:app --reload
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Visit `http://localhost:8000/auth/signin/google` to sign in.
|
|
198
|
+
|
|
199
|
+
## Configuration shortcuts
|
|
200
|
+
|
|
201
|
+
- Environment variables: `BELGIE_SECRET`, `BELGIE_BASE_URL`, `BELGIE_GOOGLE_CLIENT_ID`, `BELGIE_GOOGLE_CLIENT_SECRET`,
|
|
202
|
+
`BELGIE_GOOGLE_REDIRECT_URI` (loaded automatically by `AuthSettings()`).
|
|
203
|
+
- Session tuning: `SessionSettings(cookie_name, max_age, update_age)` controls lifetime and sliding refresh.
|
|
204
|
+
- Cookie hardening: `CookieSettings(http_only, secure, same_site)` for production-ready defaults.
|
|
205
|
+
|
|
206
|
+
## Router endpoints
|
|
207
|
+
|
|
208
|
+
- `GET /auth/signin/google` – start OAuth flow
|
|
209
|
+
- `GET /auth/callback/google` – handle Google callback
|
|
210
|
+
- `POST /auth/signout` – clear session cookie and invalidate server session
|
|
211
|
+
|
|
212
|
+
## Limitations today
|
|
213
|
+
|
|
214
|
+
- Google is the only built-in provider; more providers and email/password are on the roadmap.
|
|
215
|
+
- You manage your own database migrations and deployment (by design—no third-party control plane).
|
|
216
|
+
|
|
217
|
+
## Why teams pick Belgie
|
|
218
|
+
|
|
219
|
+
- Keep control of data and infra while getting a batteries-included OAuth flow.
|
|
220
|
+
- Minimal surface area: a single `Auth` instance exposes router + dependencies.
|
|
221
|
+
- Modern typing and clear protocols reduce integration mistakes and make refactors safer.
|
|
222
|
+
- MIT license, zero per-user costs.
|
|
223
|
+
|
|
224
|
+
## Documentation and examples
|
|
225
|
+
|
|
226
|
+
- [docs/quickstart.md](docs/quickstart.md) for full walkthrough
|
|
227
|
+
- [examples/auth](examples/auth) for a runnable app
|
|
228
|
+
|
|
229
|
+
## Contributing
|
|
230
|
+
|
|
231
|
+
MIT licensed. Issues and PRs welcome.
|
belgie-0.1.0/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Belgie
|
|
2
|
+
|
|
3
|
+
Self-hosted, type-safe authentication for FastAPI that makes Google OAuth and secure session cookies work with almost
|
|
4
|
+
zero glue code. Keep your data, skip per-user SaaS bills, and still get a polished developer experience.
|
|
5
|
+
|
|
6
|
+
## Who this is for
|
|
7
|
+
|
|
8
|
+
- FastAPI teams that want Google sign-in and protected routes today, not after weeks of wiring.
|
|
9
|
+
- Product engineers who prefer first-class type hints and adapter-driven design over magic.
|
|
10
|
+
- Startups that would rather own their user data and avoid per-MAU pricing from hosted identity vendors.
|
|
11
|
+
|
|
12
|
+
## What it solves
|
|
13
|
+
|
|
14
|
+
- End-to-end Google OAuth 2.0 flow with CSRF-safe state storage.
|
|
15
|
+
- Sliding-window, signed session cookies (no JWT juggling required).
|
|
16
|
+
- Drop-in FastAPI dependencies for `auth.user`, `auth.session`, and scoped access.
|
|
17
|
+
- A thin SQLAlchemy adapter that works with your existing models.
|
|
18
|
+
- Hooks so you can plug in logging, analytics, or audit trails without forking.
|
|
19
|
+
|
|
20
|
+
## How it compares
|
|
21
|
+
|
|
22
|
+
- **fastapi-users**: feature-rich but now in maintenance mode and optimized for password-plus-OAuth flows. Belgie
|
|
23
|
+
focuses on OAuth + session UX, keeps the surface area small, and ships type-driven adapters out of the box.
|
|
24
|
+
- **Hosted identity (Auth0, Clerk, Supabase Auth)**: great UIs and more providers, but billed per Monthly Active User
|
|
25
|
+
and hosted off your stack. Belgie is MIT-licensed, runs in your app, and never charges per user.
|
|
26
|
+
|
|
27
|
+
## Features at a glance
|
|
28
|
+
|
|
29
|
+
- Google OAuth provider with ready-made router (`/auth/signin/google`, `/auth/callback/google`, `/auth/signout`).
|
|
30
|
+
- Session manager with sliding expiry and secure cookie defaults (HttpOnly, SameSite, Secure).
|
|
31
|
+
- Scope-aware dependency for route protection (`Security(auth.user, scopes=[...])`).
|
|
32
|
+
- Modern Python (3.12+), full typing, and protocol-based models.
|
|
33
|
+
- Event hooks and utility helpers for custom workflows.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install belgie
|
|
39
|
+
# or with uv
|
|
40
|
+
uv add belgie
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For SQLAlchemy adapter support:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install belgie[alchemy]
|
|
47
|
+
# or with uv
|
|
48
|
+
uv add belgie[alchemy]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Optional extras: `belgie[mcp]`, `belgie[oauth]`, or `belgie[all]`.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
### 1) Define models
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from datetime import UTC, datetime
|
|
59
|
+
from uuid import UUID, uuid4
|
|
60
|
+
from sqlalchemy import ForeignKey, String
|
|
61
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Base(DeclarativeBase):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class User(Base):
|
|
69
|
+
__tablename__ = "users"
|
|
70
|
+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
|
71
|
+
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
|
72
|
+
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
73
|
+
image: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
74
|
+
email_verified: Mapped[bool] = mapped_column(default=False)
|
|
75
|
+
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
|
|
76
|
+
updated_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Account(Base):
|
|
80
|
+
__tablename__ = "accounts"
|
|
81
|
+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
|
82
|
+
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
|
83
|
+
provider: Mapped[str] = mapped_column(String(50))
|
|
84
|
+
provider_account_id: Mapped[str] = mapped_column(String(255))
|
|
85
|
+
access_token: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
|
86
|
+
refresh_token: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
|
87
|
+
expires_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
|
88
|
+
scope: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
89
|
+
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Session(Base):
|
|
93
|
+
__tablename__ = "sessions"
|
|
94
|
+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
|
95
|
+
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
|
96
|
+
expires_at: Mapped[datetime] = mapped_column(index=True)
|
|
97
|
+
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class OAuthState(Base):
|
|
101
|
+
__tablename__ = "oauth_states"
|
|
102
|
+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
|
103
|
+
state: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
|
104
|
+
expires_at: Mapped[datetime] = mapped_column(index=True)
|
|
105
|
+
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 2) Configure Belgie
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from belgie.auth import Auth, AuthSettings, GoogleProviderSettings
|
|
112
|
+
from belgie_alchemy import AlchemyAdapter
|
|
113
|
+
|
|
114
|
+
settings = AuthSettings(
|
|
115
|
+
secret="your-secret-key",
|
|
116
|
+
base_url="http://localhost:8000",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
adapter = AlchemyAdapter(
|
|
120
|
+
user=User,
|
|
121
|
+
account=Account,
|
|
122
|
+
session=Session,
|
|
123
|
+
oauth_state=OAuthState,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
auth = Auth(
|
|
127
|
+
settings=settings,
|
|
128
|
+
adapter=adapter,
|
|
129
|
+
providers={
|
|
130
|
+
"google": GoogleProviderSettings(
|
|
131
|
+
client_id="your-google-client-id",
|
|
132
|
+
client_secret="your-google-client-secret",
|
|
133
|
+
redirect_uri="http://localhost:8000/auth/provider/google/callback",
|
|
134
|
+
scopes=["openid", "email", "profile"],
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 3) Add routes to FastAPI
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from fastapi import Depends, FastAPI, Security
|
|
144
|
+
|
|
145
|
+
app = FastAPI()
|
|
146
|
+
app.include_router(auth.router)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.get("/")
|
|
150
|
+
async def home():
|
|
151
|
+
return {"message": "Welcome! Visit /auth/provider/google/signin to sign in"}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.get("/protected")
|
|
155
|
+
async def protected(user: User = Depends(auth.user)):
|
|
156
|
+
return {"email": user.email}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.get("/profile")
|
|
160
|
+
async def profile(user: User = Security(auth.user, scopes=["profile"])):
|
|
161
|
+
return {"name": user.name, "email": user.email}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Run it:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
uvicorn main:app --reload
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Visit `http://localhost:8000/auth/signin/google` to sign in.
|
|
171
|
+
|
|
172
|
+
## Configuration shortcuts
|
|
173
|
+
|
|
174
|
+
- Environment variables: `BELGIE_SECRET`, `BELGIE_BASE_URL`, `BELGIE_GOOGLE_CLIENT_ID`, `BELGIE_GOOGLE_CLIENT_SECRET`,
|
|
175
|
+
`BELGIE_GOOGLE_REDIRECT_URI` (loaded automatically by `AuthSettings()`).
|
|
176
|
+
- Session tuning: `SessionSettings(cookie_name, max_age, update_age)` controls lifetime and sliding refresh.
|
|
177
|
+
- Cookie hardening: `CookieSettings(http_only, secure, same_site)` for production-ready defaults.
|
|
178
|
+
|
|
179
|
+
## Router endpoints
|
|
180
|
+
|
|
181
|
+
- `GET /auth/signin/google` – start OAuth flow
|
|
182
|
+
- `GET /auth/callback/google` – handle Google callback
|
|
183
|
+
- `POST /auth/signout` – clear session cookie and invalidate server session
|
|
184
|
+
|
|
185
|
+
## Limitations today
|
|
186
|
+
|
|
187
|
+
- Google is the only built-in provider; more providers and email/password are on the roadmap.
|
|
188
|
+
- You manage your own database migrations and deployment (by design—no third-party control plane).
|
|
189
|
+
|
|
190
|
+
## Why teams pick Belgie
|
|
191
|
+
|
|
192
|
+
- Keep control of data and infra while getting a batteries-included OAuth flow.
|
|
193
|
+
- Minimal surface area: a single `Auth` instance exposes router + dependencies.
|
|
194
|
+
- Modern typing and clear protocols reduce integration mistakes and make refactors safer.
|
|
195
|
+
- MIT license, zero per-user costs.
|
|
196
|
+
|
|
197
|
+
## Documentation and examples
|
|
198
|
+
|
|
199
|
+
- [docs/quickstart.md](docs/quickstart.md) for full walkthrough
|
|
200
|
+
- [examples/auth](examples/auth) for a runnable app
|
|
201
|
+
|
|
202
|
+
## Contributing
|
|
203
|
+
|
|
204
|
+
MIT licensed. Issues and PRs welcome.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "belgie"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Modern authentication for FastAPI"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Matt LeMay", email = "mplemay@users.noreply.github.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12,<3.15"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"belgie-proto",
|
|
12
|
+
"fastapi>=0.100",
|
|
13
|
+
"httpx>=0.24",
|
|
14
|
+
"pydantic>=2.0",
|
|
15
|
+
"pydantic-settings>=2.0",
|
|
16
|
+
"python-multipart>=0.0.20",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
alchemy = [
|
|
21
|
+
"belgie-alchemy",
|
|
22
|
+
]
|
|
23
|
+
all = [
|
|
24
|
+
"belgie-alchemy",
|
|
25
|
+
"belgie-mcp",
|
|
26
|
+
"belgie-oauth",
|
|
27
|
+
]
|
|
28
|
+
examples = [
|
|
29
|
+
"uvicorn[standard]>=0.38.0",
|
|
30
|
+
]
|
|
31
|
+
mcp = [
|
|
32
|
+
"belgie-mcp",
|
|
33
|
+
]
|
|
34
|
+
oauth = [
|
|
35
|
+
"belgie-oauth",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["uv_build>=0.9.10,<0.10.0"]
|
|
40
|
+
build-backend = "uv_build"
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"aiosqlite>=0.21.0",
|
|
45
|
+
"belgie-alchemy",
|
|
46
|
+
"pre-commit>=4.4.0",
|
|
47
|
+
"pytest>=9.0.1",
|
|
48
|
+
"pytest-asyncio>=1.3.0",
|
|
49
|
+
"pytest-cov>=7.0.0",
|
|
50
|
+
"respx>=0.22.0",
|
|
51
|
+
"ruff>=0.14.5",
|
|
52
|
+
"rumdl",
|
|
53
|
+
"ty>=0.0.1a27",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.uv.sources]
|
|
57
|
+
belgie-alchemy = { workspace = true }
|
|
58
|
+
belgie-mcp = { workspace = true }
|
|
59
|
+
belgie-oauth = { workspace = true }
|
|
60
|
+
belgie-proto = { workspace = true }
|
|
61
|
+
|
|
62
|
+
[tool.uv.workspace]
|
|
63
|
+
members = [
|
|
64
|
+
"packages/belgie-alchemy",
|
|
65
|
+
"packages/belgie-mcp",
|
|
66
|
+
"packages/belgie-oauth",
|
|
67
|
+
"packages/belgie-proto",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
target-version = "py312"
|
|
72
|
+
line-length = 120
|
|
73
|
+
include = ["pyproject.toml"]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint]
|
|
76
|
+
select = ["ALL"]
|
|
77
|
+
ignore = [
|
|
78
|
+
"D", # pydocstyle (i.e. ignore missing/malformed docstrings)
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint.per-file-ignores]
|
|
82
|
+
"__init__.py" = [
|
|
83
|
+
"F401", # imported but unused (common in __init__.py to re-export symbols)
|
|
84
|
+
]
|
|
85
|
+
"**/tests/**/*" = [
|
|
86
|
+
"ANN001", # missing type annotation for function argument
|
|
87
|
+
"ANN201", # missing return type annotation for public function
|
|
88
|
+
"ANN202", # missing return type annotation for private function
|
|
89
|
+
"B008", # function call in argument defaults (required for FastAPI Depends)
|
|
90
|
+
"F841", # local variable assigned but never used
|
|
91
|
+
"FAST002", # FastAPI dependency without Annotated
|
|
92
|
+
"INP001", # implicit namespace package (tests dir may not need __init__.py)
|
|
93
|
+
"PLR0913", # too many arguments in function definition
|
|
94
|
+
"PLR2004", # magic value used in comparison (fine in test cases)
|
|
95
|
+
"S101", # use of assert (tests typically use assert directly)
|
|
96
|
+
"S106", # possible hardcoded password (test secrets are fine)
|
|
97
|
+
"SLF001", # private member accessed
|
|
98
|
+
]
|
|
99
|
+
"**/__tests__/**/*" = [
|
|
100
|
+
"ANN001", # missing type annotation for function argument
|
|
101
|
+
"ANN201", # missing return type annotation for public function
|
|
102
|
+
"ANN202", # missing return type annotation for private function
|
|
103
|
+
"B008", # function call in argument defaults (required for FastAPI Depends)
|
|
104
|
+
"F841", # local variable assigned but never used
|
|
105
|
+
"FAST002", # FastAPI dependency without Annotated
|
|
106
|
+
"INP001", # implicit namespace package (tests dir may not need __init__.py)
|
|
107
|
+
"PLR0913", # too many arguments in function definition
|
|
108
|
+
"PLR2004", # magic value used in comparison (fine in test cases)
|
|
109
|
+
"S101", # use of assert (tests typically use assert directly)
|
|
110
|
+
"S106", # possible hardcoded password (test secrets are fine)
|
|
111
|
+
"SLF001", # private member accessed
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
[tool.ruff.lint.flake8-quotes]
|
|
115
|
+
inline-quotes = "double"
|
|
116
|
+
multiline-quotes = "double"
|
|
117
|
+
|
|
118
|
+
[tool.ruff.lint.isort]
|
|
119
|
+
combine-as-imports = true
|
|
120
|
+
|
|
121
|
+
[tool.ruff.format]
|
|
122
|
+
docstring-code-format = true
|
|
123
|
+
quote-style = "double"
|
|
124
|
+
|
|
125
|
+
[tool.pytest.ini_options]
|
|
126
|
+
asyncio_mode = "strict"
|
|
127
|
+
markers = [
|
|
128
|
+
"integration: marks tests that hit the database or external services",
|
|
129
|
+
]
|
|
130
|
+
filterwarnings = [
|
|
131
|
+
"error",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
[tool.rumdl.MD013]
|
|
135
|
+
line_length = 120
|
|
136
|
+
reflow = true
|
|
137
|
+
code_blocks = false
|
|
138
|
+
tables = false
|
|
Binary file
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Belgie - Modern authentication and analytics for FastAPI."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from belgie.auth import (
|
|
7
|
+
Auth,
|
|
8
|
+
AuthClient,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
AuthorizationError,
|
|
11
|
+
AuthSettings,
|
|
12
|
+
BelgieError,
|
|
13
|
+
ConfigurationError,
|
|
14
|
+
CookieSettings,
|
|
15
|
+
DBConnection,
|
|
16
|
+
GoogleOAuthProvider,
|
|
17
|
+
GoogleProviderSettings,
|
|
18
|
+
GoogleUserInfo,
|
|
19
|
+
HookContext,
|
|
20
|
+
HookEvent,
|
|
21
|
+
HookRunner,
|
|
22
|
+
Hooks,
|
|
23
|
+
InvalidStateError,
|
|
24
|
+
OAuthError,
|
|
25
|
+
OAuthProviderProtocol,
|
|
26
|
+
Providers,
|
|
27
|
+
SessionExpiredError,
|
|
28
|
+
SessionManager,
|
|
29
|
+
SessionSettings,
|
|
30
|
+
URLSettings,
|
|
31
|
+
generate_session_id,
|
|
32
|
+
generate_state_token,
|
|
33
|
+
parse_scopes,
|
|
34
|
+
validate_scopes,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from belgie_alchemy import AlchemyAdapter as AlchemyAdapter
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
41
|
+
|
|
42
|
+
_ALCHEMY_IMPORT_ERROR = "AlchemyAdapter requires the 'alchemy' extra. Install with: uv add belgie[alchemy]"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __getattr__(name: str) -> object:
|
|
46
|
+
if name != "AlchemyAdapter":
|
|
47
|
+
msg = f"module 'belgie' has no attribute {name!r}"
|
|
48
|
+
raise AttributeError(msg)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
module = import_module("belgie_alchemy")
|
|
52
|
+
except ModuleNotFoundError as exc:
|
|
53
|
+
raise ImportError(_ALCHEMY_IMPORT_ERROR) from exc
|
|
54
|
+
|
|
55
|
+
return module.AlchemyAdapter
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = [ # noqa: RUF022
|
|
59
|
+
# Version
|
|
60
|
+
"__version__",
|
|
61
|
+
# Core
|
|
62
|
+
"Auth",
|
|
63
|
+
"AuthClient",
|
|
64
|
+
"AuthSettings",
|
|
65
|
+
"Hooks",
|
|
66
|
+
"HookContext",
|
|
67
|
+
"HookEvent",
|
|
68
|
+
"HookRunner",
|
|
69
|
+
# Adapters
|
|
70
|
+
"AlchemyAdapter",
|
|
71
|
+
"DBConnection",
|
|
72
|
+
# Session
|
|
73
|
+
"SessionManager",
|
|
74
|
+
# Providers
|
|
75
|
+
"GoogleOAuthProvider",
|
|
76
|
+
"GoogleProviderSettings",
|
|
77
|
+
"GoogleUserInfo",
|
|
78
|
+
"OAuthProviderProtocol",
|
|
79
|
+
"Providers",
|
|
80
|
+
# Settings
|
|
81
|
+
"SessionSettings",
|
|
82
|
+
"CookieSettings",
|
|
83
|
+
"URLSettings",
|
|
84
|
+
# Exceptions
|
|
85
|
+
"BelgieError",
|
|
86
|
+
"AuthenticationError",
|
|
87
|
+
"AuthorizationError",
|
|
88
|
+
"SessionExpiredError",
|
|
89
|
+
"InvalidStateError",
|
|
90
|
+
"OAuthError",
|
|
91
|
+
"ConfigurationError",
|
|
92
|
+
# Utils
|
|
93
|
+
"generate_session_id",
|
|
94
|
+
"generate_state_token",
|
|
95
|
+
"parse_scopes",
|
|
96
|
+
"validate_scopes",
|
|
97
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Alchemy re-exports for belgie consumers."""
|
|
2
|
+
|
|
3
|
+
_ALCHEMY_IMPORT_ERROR = "belgie.alchemy requires the 'alchemy' extra. Install with: uv add belgie[alchemy]"
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from belgie_alchemy import ( # type: ignore[import-not-found]
|
|
7
|
+
AlchemyAdapter,
|
|
8
|
+
Base,
|
|
9
|
+
DatabaseSettings,
|
|
10
|
+
DateTimeUTC,
|
|
11
|
+
PrimaryKeyMixin,
|
|
12
|
+
TimestampMixin,
|
|
13
|
+
)
|
|
14
|
+
except ModuleNotFoundError as exc:
|
|
15
|
+
raise ImportError(_ALCHEMY_IMPORT_ERROR) from exc
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AlchemyAdapter",
|
|
19
|
+
"Base",
|
|
20
|
+
"DatabaseSettings",
|
|
21
|
+
"DateTimeUTC",
|
|
22
|
+
"PrimaryKeyMixin",
|
|
23
|
+
"TimestampMixin",
|
|
24
|
+
]
|