cc-auth 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,32 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ publish:
10
+ name: Build & publish to PyPI
11
+ runs-on: ubuntu-latest
12
+ environment: pypi
13
+ permissions:
14
+ contents: read
15
+ id-token: write # required for trusted publishing (no API token needed)
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: '3.11'
24
+
25
+ - name: Install build tools
26
+ run: pip install build
27
+
28
+ - name: Build package
29
+ run: python -m build
30
+
31
+ - name: Publish to PyPI
32
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,28 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: Python ${{ matrix.python-version }}
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.9", "3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: pip install -e ".[dev]"
26
+
27
+ - name: Run tests
28
+ run: pytest --tb=short -v
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+ .env
10
+ .pytest_cache/
11
+ .coverage
12
+ htmlcov/
cc_auth-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.4
2
+ Name: cc-auth
3
+ Version: 0.1.0
4
+ Summary: Cloudflare Access JWT validation + RBAC for Python backends
5
+ Project-URL: Homepage, https://github.com/computacenter-ro/cc-auth
6
+ Project-URL: Repository, https://github.com/computacenter-ro/cc-auth
7
+ Project-URL: Bug Tracker, https://github.com/computacenter-ro/cc-auth/issues
8
+ License: MIT
9
+ Keywords: authentication,cloudflare,cloudflare-access,fastapi,jwt,rbac
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: FastAPI
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
20
+ Classifier: Topic :: Security
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: pyjwt[crypto]>=2.8.0
24
+ Requires-Dist: requests>=2.31.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: cryptography>=42.0; extra == 'dev'
27
+ Requires-Dist: fastapi>=0.100.0; extra == 'dev'
28
+ Requires-Dist: httpx>=0.27; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: starlette>=0.27.0; extra == 'dev'
32
+ Provides-Extra: fastapi
33
+ Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
34
+ Requires-Dist: starlette>=0.27.0; extra == 'fastapi'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # cc-auth
38
+
39
+ Python library for validating [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/) JWTs and enforcing role-based access control (RBAC) in backend services.
40
+
41
+ Every service behind a Cloudflare Access Application receives a signed JWT on every request (`CF-Access-Jwt-Assertion` header / `CF_Authorization` cookie). This library:
42
+
43
+ 1. **Validates** the JWT signature against Cloudflare's public keys (JWKS)
44
+ 2. **Extracts** identity — email, name, Entra ID group memberships
45
+ 3. **Maps** Entra ID groups → application roles
46
+ 4. Provides **FastAPI middleware and dependencies** so routes declare their role requirements in one line
47
+
48
+ ---
49
+
50
+ ## Documentation
51
+
52
+ | Document | Description |
53
+ |---|---|
54
+ | [Azure + Cloudflare group sources](docs/azure-cloudflare-group-sources.md) | Deep dive into the two mechanisms that supply group membership (Microsoft Graph vs ID Token claim), why both exist, and which one `cc-auth` relies on |
55
+ | [ADR-001 — Group claims strategy](docs/adr/001-group-claims-strategy.md) | Architecture Decision Record: why Source A (Support Groups) is the canonical group source, and the trade-offs accepted |
56
+
57
+ ---
58
+
59
+ ## Architecture
60
+
61
+ ```
62
+ Microsoft Entra ID
63
+
64
+ │ User authenticates (OIDC)
65
+
66
+ Cloudflare Zero Trust (Access Application)
67
+
68
+ │ Issues signed JWT containing:
69
+ │ { "email": "user@cc.com",
70
+ │ "groups": ["group-id-1", "group-id-2"],
71
+ │ "iss": "https://<team-domain>" }
72
+
73
+ │ JWT travels as header + cookie on every request
74
+
75
+ Your FastAPI backend ◄──── cc-auth validates + maps groups → roles
76
+
77
+ │ Route reads User.roles for RBAC decisions
78
+
79
+ Business logic
80
+ ```
81
+
82
+ **Important:** the backend must always validate the JWT itself. Cloudflare validates at the edge, but if your origin IP is ever reachable directly (bypassing Cloudflare), only your backend's validation stands.
83
+
84
+ ---
85
+
86
+ ## Installation
87
+
88
+ ```bash
89
+ pip install cc-auth
90
+ # or with FastAPI middleware helpers:
91
+ pip install "cc-auth[fastapi]"
92
+ ```
93
+
94
+ Pin a specific version for reproducible production builds:
95
+ ```
96
+ cc-auth[fastapi]==0.1.0
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Quickstart (FastAPI)
102
+
103
+ ### 1. Enable group claims (one-time, per Zero Trust account)
104
+
105
+ In Zero Trust → Settings → Authentication → your Azure AD identity provider → **Edit**:
106
+ - Enable **"Support groups"**
107
+
108
+ This makes Cloudflare include the user's Entra ID group Object IDs in every JWT.
109
+
110
+ ### 2. Add the library to your app
111
+
112
+ ```python
113
+ # main.py
114
+ from fastapi import FastAPI
115
+ from cc_auth import CloudflareAuth
116
+
117
+ auth = CloudflareAuth.from_env()
118
+
119
+ app = FastAPI()
120
+ app.add_middleware(auth.middleware(exclude_paths=["/health"]))
121
+ ```
122
+
123
+ No configuration is required to get started. Add `CF_ACCESS_GROUP_MAPPING` when you
124
+ need role-based access control (see [Environment variables](#environment-variables)).
125
+
126
+ ### 3. Use in routes
127
+
128
+ Most routes need **nothing extra** — the middleware already blocks unauthenticated
129
+ requests. Only add `Depends(...)` when you need the user object or role checks inside
130
+ the handler:
131
+
132
+ ```python
133
+ from cc_auth import User
134
+
135
+ # Protected automatically by middleware — no Depends needed
136
+ @app.get("/items")
137
+ async def list_items():
138
+ return [...]
139
+
140
+ # Need to know who the user is
141
+ @app.get("/me")
142
+ async def me(user: User = Depends(auth.current_user)):
143
+ return {"email": user.email, "roles": user.roles}
144
+
145
+ # Restrict to admins only
146
+ @app.delete("/items/{id}")
147
+ async def delete_item(id: str, user: User = Depends(auth.require_role("admin"))):
148
+ ...
149
+
150
+ # Restrict to admins or editors
151
+ @app.post("/items")
152
+ async def create_item(user: User = Depends(auth.require_any_role(["admin", "editor"]))):
153
+ ...
154
+
155
+ # Excluded from auth — open to anyone
156
+ @app.get("/health")
157
+ async def health():
158
+ return {"status": "ok"}
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Configuration reference
164
+
165
+ ### `CloudflareAuth(team_domain, role_groups=None)`
166
+
167
+ | Parameter | Type | Required | Description |
168
+ |---|---|---|---|
169
+ | `team_domain` | `str` | **Yes** | Your Cloudflare Zero Trust team domain, e.g. `your-org.cloudflareaccess.com` |
170
+ | `role_groups` | `dict[str, str]` | No | Map of Entra ID group Object ID → role name |
171
+
172
+ ---
173
+
174
+ ### `User` object
175
+
176
+ | Field | Type | Description |
177
+ |---|---|---|
178
+ | `sub` | `str` | Cloudflare user identifier (stable across sessions) |
179
+ | `email` | `str` | User's email address |
180
+ | `name` | `str` | Display name (falls back to email) |
181
+ | `groups` | `list[str]` | Entra ID group Object IDs — raw, from JWT |
182
+ | `roles` | `list[str]` | Role names mapped from groups via `role_groups` |
183
+ | `raw` | `dict` | Full decoded JWT payload |
184
+
185
+ Helper methods:
186
+ - `user.has_role("admin")` → `bool`
187
+ - `user.has_any_role(["admin", "editor"])` → `bool`
188
+ - `user.has_all_roles(["admin", "editor"])` → `bool`
189
+
190
+ ---
191
+
192
+ ## Minimal setup (no RBAC)
193
+
194
+ If your app only needs SSO (any authenticated user), no environment variables are needed:
195
+
196
+ ```python
197
+ auth = CloudflareAuth.from_env()
198
+
199
+ app.add_middleware(auth.middleware()) # protects every route
200
+
201
+ @app.get("/data")
202
+ async def data():
203
+ return [...]
204
+
205
+ @app.get("/me")
206
+ async def me(user: User = Depends(auth.current_user)):
207
+ return {"email": user.email}
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Health checks and excluded paths
213
+
214
+ Skip auth for paths that must be publicly reachable:
215
+
216
+ ```python
217
+ app.add_middleware(auth.middleware(exclude_paths=["/health", "/metrics"]))
218
+ ```
219
+
220
+ Path matching is prefix-based: `/health` excludes `/health`, `/health/live`, `/healthz`, etc.
221
+
222
+ ---
223
+
224
+ ## Environment variables
225
+
226
+ `CloudflareAuth.from_env()` reads the following environment variables:
227
+
228
+ | Variable | Description |
229
+ |---|---|
230
+ | `CF_ACCESS_GROUP_MAPPING` | JSON `{"role_name": "group-object-id", ...}`. Role name is the key; group Object ID is the value. The library inverts this internally. |
231
+ | `CF_ACCESS_REQUIRED_GROUPS` | Comma-separated group Object IDs. Each maps to the built-in role `"member"`. Use for simple allow/deny without named roles. |
232
+ | `CF_TEAM_DOMAIN` | **Required for `from_env()`**. Your Cloudflare Zero Trust team domain, e.g. `your-org.cloudflareaccess.com`. |
233
+
234
+ Both `CF_ACCESS_GROUP_MAPPING` and `CF_ACCESS_REQUIRED_GROUPS` may be set at the same
235
+ time. If the same group Object ID appears in both, `CF_ACCESS_GROUP_MAPPING` wins.
236
+
237
+ ### Simple allow/deny (no named roles)
238
+
239
+ ```bash
240
+ CF_ACCESS_REQUIRED_GROUPS=4fb32c31-b135-43d5-a00e-9e566e6aceff,6543851f-fd97-40e8-b097-ab5a71e44ef2
241
+ ```
242
+
243
+ ```python
244
+ auth = CloudflareAuth.from_env()
245
+
246
+ @app.get("/data")
247
+ async def data(user: User = Depends(auth.require_role("member"))):
248
+ ...
249
+ ```
250
+
251
+ ### Named roles
252
+
253
+ ```bash
254
+ CF_ACCESS_GROUP_MAPPING={"admins":"4fb32c31-...","developers":"6543851f-..."}
255
+ ```
256
+
257
+ ```python
258
+ auth = CloudflareAuth.from_env()
259
+
260
+ @app.delete("/items/{id}")
261
+ async def delete(id: str, user: User = Depends(auth.require_role("admins"))):
262
+ ...
263
+ ```
264
+
265
+ ---
266
+
267
+ ## How it works under the hood
268
+
269
+ 1. On the first request, `PyJWKClient` fetches `https://<team-domain>/cdn-cgi/access/certs` and caches the RSA public keys in memory.
270
+ 2. For each request: the token's `kid` header selects the right public key from the cache; `PyJWT` verifies the RS256 signature, `iss`, and `exp`.
271
+ 3. Groups are read from the `groups` claim — an array of Entra ID group Object IDs added by Cloudflare when "Support groups" is enabled on the Azure AD identity provider.
272
+ 4. Role mapping is a simple dict lookup: O(1) per group.
273
+
274
+ ---
275
+
276
+ ## Testing
277
+
278
+ ```bash
279
+ pip install -e ".[dev]"
280
+ pytest
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Adding to `requirements.txt`
286
+
287
+ ```
288
+ cc-auth[fastapi]==0.1.0
289
+ ```
290
+
291
+ Pin to a specific version for reproducible production builds. Check [PyPI](https://pypi.org/project/cc-auth/) for the latest version.
@@ -0,0 +1,255 @@
1
+ # cc-auth
2
+
3
+ Python library for validating [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/) JWTs and enforcing role-based access control (RBAC) in backend services.
4
+
5
+ Every service behind a Cloudflare Access Application receives a signed JWT on every request (`CF-Access-Jwt-Assertion` header / `CF_Authorization` cookie). This library:
6
+
7
+ 1. **Validates** the JWT signature against Cloudflare's public keys (JWKS)
8
+ 2. **Extracts** identity — email, name, Entra ID group memberships
9
+ 3. **Maps** Entra ID groups → application roles
10
+ 4. Provides **FastAPI middleware and dependencies** so routes declare their role requirements in one line
11
+
12
+ ---
13
+
14
+ ## Documentation
15
+
16
+ | Document | Description |
17
+ |---|---|
18
+ | [Azure + Cloudflare group sources](docs/azure-cloudflare-group-sources.md) | Deep dive into the two mechanisms that supply group membership (Microsoft Graph vs ID Token claim), why both exist, and which one `cc-auth` relies on |
19
+ | [ADR-001 — Group claims strategy](docs/adr/001-group-claims-strategy.md) | Architecture Decision Record: why Source A (Support Groups) is the canonical group source, and the trade-offs accepted |
20
+
21
+ ---
22
+
23
+ ## Architecture
24
+
25
+ ```
26
+ Microsoft Entra ID
27
+
28
+ │ User authenticates (OIDC)
29
+
30
+ Cloudflare Zero Trust (Access Application)
31
+
32
+ │ Issues signed JWT containing:
33
+ │ { "email": "user@cc.com",
34
+ │ "groups": ["group-id-1", "group-id-2"],
35
+ │ "iss": "https://<team-domain>" }
36
+
37
+ │ JWT travels as header + cookie on every request
38
+
39
+ Your FastAPI backend ◄──── cc-auth validates + maps groups → roles
40
+
41
+ │ Route reads User.roles for RBAC decisions
42
+
43
+ Business logic
44
+ ```
45
+
46
+ **Important:** the backend must always validate the JWT itself. Cloudflare validates at the edge, but if your origin IP is ever reachable directly (bypassing Cloudflare), only your backend's validation stands.
47
+
48
+ ---
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install cc-auth
54
+ # or with FastAPI middleware helpers:
55
+ pip install "cc-auth[fastapi]"
56
+ ```
57
+
58
+ Pin a specific version for reproducible production builds:
59
+ ```
60
+ cc-auth[fastapi]==0.1.0
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Quickstart (FastAPI)
66
+
67
+ ### 1. Enable group claims (one-time, per Zero Trust account)
68
+
69
+ In Zero Trust → Settings → Authentication → your Azure AD identity provider → **Edit**:
70
+ - Enable **"Support groups"**
71
+
72
+ This makes Cloudflare include the user's Entra ID group Object IDs in every JWT.
73
+
74
+ ### 2. Add the library to your app
75
+
76
+ ```python
77
+ # main.py
78
+ from fastapi import FastAPI
79
+ from cc_auth import CloudflareAuth
80
+
81
+ auth = CloudflareAuth.from_env()
82
+
83
+ app = FastAPI()
84
+ app.add_middleware(auth.middleware(exclude_paths=["/health"]))
85
+ ```
86
+
87
+ No configuration is required to get started. Add `CF_ACCESS_GROUP_MAPPING` when you
88
+ need role-based access control (see [Environment variables](#environment-variables)).
89
+
90
+ ### 3. Use in routes
91
+
92
+ Most routes need **nothing extra** — the middleware already blocks unauthenticated
93
+ requests. Only add `Depends(...)` when you need the user object or role checks inside
94
+ the handler:
95
+
96
+ ```python
97
+ from cc_auth import User
98
+
99
+ # Protected automatically by middleware — no Depends needed
100
+ @app.get("/items")
101
+ async def list_items():
102
+ return [...]
103
+
104
+ # Need to know who the user is
105
+ @app.get("/me")
106
+ async def me(user: User = Depends(auth.current_user)):
107
+ return {"email": user.email, "roles": user.roles}
108
+
109
+ # Restrict to admins only
110
+ @app.delete("/items/{id}")
111
+ async def delete_item(id: str, user: User = Depends(auth.require_role("admin"))):
112
+ ...
113
+
114
+ # Restrict to admins or editors
115
+ @app.post("/items")
116
+ async def create_item(user: User = Depends(auth.require_any_role(["admin", "editor"]))):
117
+ ...
118
+
119
+ # Excluded from auth — open to anyone
120
+ @app.get("/health")
121
+ async def health():
122
+ return {"status": "ok"}
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Configuration reference
128
+
129
+ ### `CloudflareAuth(team_domain, role_groups=None)`
130
+
131
+ | Parameter | Type | Required | Description |
132
+ |---|---|---|---|
133
+ | `team_domain` | `str` | **Yes** | Your Cloudflare Zero Trust team domain, e.g. `your-org.cloudflareaccess.com` |
134
+ | `role_groups` | `dict[str, str]` | No | Map of Entra ID group Object ID → role name |
135
+
136
+ ---
137
+
138
+ ### `User` object
139
+
140
+ | Field | Type | Description |
141
+ |---|---|---|
142
+ | `sub` | `str` | Cloudflare user identifier (stable across sessions) |
143
+ | `email` | `str` | User's email address |
144
+ | `name` | `str` | Display name (falls back to email) |
145
+ | `groups` | `list[str]` | Entra ID group Object IDs — raw, from JWT |
146
+ | `roles` | `list[str]` | Role names mapped from groups via `role_groups` |
147
+ | `raw` | `dict` | Full decoded JWT payload |
148
+
149
+ Helper methods:
150
+ - `user.has_role("admin")` → `bool`
151
+ - `user.has_any_role(["admin", "editor"])` → `bool`
152
+ - `user.has_all_roles(["admin", "editor"])` → `bool`
153
+
154
+ ---
155
+
156
+ ## Minimal setup (no RBAC)
157
+
158
+ If your app only needs SSO (any authenticated user), no environment variables are needed:
159
+
160
+ ```python
161
+ auth = CloudflareAuth.from_env()
162
+
163
+ app.add_middleware(auth.middleware()) # protects every route
164
+
165
+ @app.get("/data")
166
+ async def data():
167
+ return [...]
168
+
169
+ @app.get("/me")
170
+ async def me(user: User = Depends(auth.current_user)):
171
+ return {"email": user.email}
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Health checks and excluded paths
177
+
178
+ Skip auth for paths that must be publicly reachable:
179
+
180
+ ```python
181
+ app.add_middleware(auth.middleware(exclude_paths=["/health", "/metrics"]))
182
+ ```
183
+
184
+ Path matching is prefix-based: `/health` excludes `/health`, `/health/live`, `/healthz`, etc.
185
+
186
+ ---
187
+
188
+ ## Environment variables
189
+
190
+ `CloudflareAuth.from_env()` reads the following environment variables:
191
+
192
+ | Variable | Description |
193
+ |---|---|
194
+ | `CF_ACCESS_GROUP_MAPPING` | JSON `{"role_name": "group-object-id", ...}`. Role name is the key; group Object ID is the value. The library inverts this internally. |
195
+ | `CF_ACCESS_REQUIRED_GROUPS` | Comma-separated group Object IDs. Each maps to the built-in role `"member"`. Use for simple allow/deny without named roles. |
196
+ | `CF_TEAM_DOMAIN` | **Required for `from_env()`**. Your Cloudflare Zero Trust team domain, e.g. `your-org.cloudflareaccess.com`. |
197
+
198
+ Both `CF_ACCESS_GROUP_MAPPING` and `CF_ACCESS_REQUIRED_GROUPS` may be set at the same
199
+ time. If the same group Object ID appears in both, `CF_ACCESS_GROUP_MAPPING` wins.
200
+
201
+ ### Simple allow/deny (no named roles)
202
+
203
+ ```bash
204
+ CF_ACCESS_REQUIRED_GROUPS=4fb32c31-b135-43d5-a00e-9e566e6aceff,6543851f-fd97-40e8-b097-ab5a71e44ef2
205
+ ```
206
+
207
+ ```python
208
+ auth = CloudflareAuth.from_env()
209
+
210
+ @app.get("/data")
211
+ async def data(user: User = Depends(auth.require_role("member"))):
212
+ ...
213
+ ```
214
+
215
+ ### Named roles
216
+
217
+ ```bash
218
+ CF_ACCESS_GROUP_MAPPING={"admins":"4fb32c31-...","developers":"6543851f-..."}
219
+ ```
220
+
221
+ ```python
222
+ auth = CloudflareAuth.from_env()
223
+
224
+ @app.delete("/items/{id}")
225
+ async def delete(id: str, user: User = Depends(auth.require_role("admins"))):
226
+ ...
227
+ ```
228
+
229
+ ---
230
+
231
+ ## How it works under the hood
232
+
233
+ 1. On the first request, `PyJWKClient` fetches `https://<team-domain>/cdn-cgi/access/certs` and caches the RSA public keys in memory.
234
+ 2. For each request: the token's `kid` header selects the right public key from the cache; `PyJWT` verifies the RS256 signature, `iss`, and `exp`.
235
+ 3. Groups are read from the `groups` claim — an array of Entra ID group Object IDs added by Cloudflare when "Support groups" is enabled on the Azure AD identity provider.
236
+ 4. Role mapping is a simple dict lookup: O(1) per group.
237
+
238
+ ---
239
+
240
+ ## Testing
241
+
242
+ ```bash
243
+ pip install -e ".[dev]"
244
+ pytest
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Adding to `requirements.txt`
250
+
251
+ ```
252
+ cc-auth[fastapi]==0.1.0
253
+ ```
254
+
255
+ Pin to a specific version for reproducible production builds. Check [PyPI](https://pypi.org/project/cc-auth/) for the latest version.
@@ -0,0 +1,30 @@
1
+ """
2
+ cc-auth — Cloudflare Access JWT validation + RBAC for Python backends.
3
+
4
+ Quickstart::
5
+
6
+ from cc_auth import CloudflareAuth, User
7
+
8
+ # No configuration required to get started.
9
+ # Set CF_ACCESS_GROUP_MAPPING for role-based access control.
10
+ auth = CloudflareAuth.from_env()
11
+
12
+ # FastAPI
13
+ app.add_middleware(auth.middleware(exclude_paths=["/health"]))
14
+
15
+ @router.get("/me")
16
+ async def me(user: User = Depends(auth.current_user)):
17
+ return {"email": user.email, "roles": user.roles}
18
+
19
+ @router.delete("/item/{id}")
20
+ async def delete(id: str, user: User = Depends(auth.require_role("admin"))):
21
+ ...
22
+ """
23
+
24
+ from .core import CloudflareAuth
25
+ from .exceptions import AuthError
26
+ from .exceptions import PermissionError as CCPermissionError
27
+ from .identity import User
28
+
29
+ __all__ = ["CloudflareAuth", "User", "AuthError", "CCPermissionError"]
30
+ __version__ = "0.1.0"