Skip to main content

Cloudflare Access JWT validation + RBAC for Python backends

Project description

cc-auth

Python library for validating Cloudflare Access JWTs and enforcing role-based access control (RBAC) in backend services.

Every service behind a Cloudflare Access Application receives a signed JWT on every request (CF-Access-Jwt-Assertion header / CF_Authorization cookie). This library:

  1. Validates the JWT signature against Cloudflare's public keys (JWKS)
  2. Extracts identity — email, name, Entra ID group memberships
  3. Maps Entra ID groups → application roles
  4. Provides FastAPI middleware and dependencies so routes declare their role requirements in one line

Documentation

Document Description
Azure + Cloudflare group sources 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
ADR-001 — Group claims strategy Architecture Decision Record: why Source A (Support Groups) is the canonical group source, and the trade-offs accepted

Architecture

Microsoft Entra ID
      │
      │  User authenticates (OIDC)
      ▼
Cloudflare Zero Trust (Access Application)
      │
      │  Issues signed JWT containing:
      │    { "email": "user@cc.com",
      │      "groups": ["group-id-1", "group-id-2"],
      │      "iss": "https://<team-domain>" }
      │
      │  JWT travels as header + cookie on every request
      ▼
Your FastAPI backend  ◄──── cc-auth validates + maps groups → roles
      │
      │  Route reads User.roles for RBAC decisions
      ▼
Business logic

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.


Installation

pip install cc-auth
# or with FastAPI middleware helpers:
pip install "cc-auth[fastapi]"

Pin a specific version for reproducible production builds:

cc-auth[fastapi]==0.1.0

Quickstart (FastAPI)

1. Enable group claims (one-time, per Zero Trust account)

In Zero Trust → Settings → Authentication → your Azure AD identity provider → Edit:

  • Enable "Support groups"

This makes Cloudflare include the user's Entra ID group Object IDs in every JWT.

2. Add the library to your app

# main.py
from fastapi import FastAPI
from cc_auth import CloudflareAuth

auth = CloudflareAuth.from_env()

app = FastAPI()
app.add_middleware(auth.middleware(exclude_paths=["/health"]))

No configuration is required to get started. Add CF_ACCESS_GROUP_MAPPING when you need role-based access control (see Environment variables).

3. Use in routes

Most routes need nothing extra — the middleware already blocks unauthenticated requests. Only add Depends(...) when you need the user object or role checks inside the handler:

from cc_auth import User

# Protected automatically by middleware — no Depends needed
@app.get("/items")
async def list_items():
    return [...]

# Need to know who the user is
@app.get("/me")
async def me(user: User = Depends(auth.current_user)):
    return {"email": user.email, "roles": user.roles}

# Restrict to admins only
@app.delete("/items/{id}")
async def delete_item(id: str, user: User = Depends(auth.require_role("admin"))):
    ...

# Restrict to admins or editors
@app.post("/items")
async def create_item(user: User = Depends(auth.require_any_role(["admin", "editor"]))):
    ...

# Excluded from auth — open to anyone
@app.get("/health")
async def health():
    return {"status": "ok"}

Configuration reference

CloudflareAuth(team_domain, role_groups=None)

Parameter Type Required Description
team_domain str Yes Your Cloudflare Zero Trust team domain, e.g. your-org.cloudflareaccess.com
role_groups dict[str, str] No Map of Entra ID group Object ID → role name

User object

Field Type Description
sub str Cloudflare user identifier (stable across sessions)
email str User's email address
name str Display name (falls back to email)
groups list[str] Entra ID group Object IDs — raw, from JWT
roles list[str] Role names mapped from groups via role_groups
raw dict Full decoded JWT payload

Helper methods:

  • user.has_role("admin")bool
  • user.has_any_role(["admin", "editor"])bool
  • user.has_all_roles(["admin", "editor"])bool

Minimal setup (no RBAC)

If your app only needs SSO (any authenticated user), no environment variables are needed:

auth = CloudflareAuth.from_env()

app.add_middleware(auth.middleware())   # protects every route

@app.get("/data")
async def data():
    return [...]

@app.get("/me")
async def me(user: User = Depends(auth.current_user)):
    return {"email": user.email}

Health checks and excluded paths

Skip auth for paths that must be publicly reachable:

app.add_middleware(auth.middleware(exclude_paths=["/health", "/metrics"]))

Path matching is prefix-based: /health excludes /health, /health/live, /healthz, etc.


Environment variables

CloudflareAuth.from_env() reads the following environment variables:

Variable Description
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.
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.
CF_TEAM_DOMAIN Required for from_env(). Your Cloudflare Zero Trust team domain, e.g. your-org.cloudflareaccess.com.

Both CF_ACCESS_GROUP_MAPPING and CF_ACCESS_REQUIRED_GROUPS may be set at the same time. If the same group Object ID appears in both, CF_ACCESS_GROUP_MAPPING wins.

Simple allow/deny (no named roles)

CF_ACCESS_REQUIRED_GROUPS=4fb32c31-b135-43d5-a00e-9e566e6aceff,6543851f-fd97-40e8-b097-ab5a71e44ef2
auth = CloudflareAuth.from_env()

@app.get("/data")
async def data(user: User = Depends(auth.require_role("member"))):
    ...

Named roles

CF_ACCESS_GROUP_MAPPING={"admins":"4fb32c31-...","developers":"6543851f-..."}
auth = CloudflareAuth.from_env()

@app.delete("/items/{id}")
async def delete(id: str, user: User = Depends(auth.require_role("admins"))):
    ...

How it works under the hood

  1. On the first request, PyJWKClient fetches https://<team-domain>/cdn-cgi/access/certs and caches the RSA public keys in memory.
  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.
  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.
  4. Role mapping is a simple dict lookup: O(1) per group.

Testing

pip install -e ".[dev]"
pytest

Adding to requirements.txt

cc-auth[fastapi]==0.1.0

Pin to a specific version for reproducible production builds. Check PyPI for the latest version.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cc_auth-0.1.0.tar.gz (18.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cc_auth-0.1.0-py3-none-any.whl (13.6 kB view details)

Uploaded Python 3

File details

Details for the file cc_auth-0.1.0.tar.gz.

File metadata

  • Download URL: cc_auth-0.1.0.tar.gz
  • Upload date:
  • Size: 18.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cc_auth-0.1.0.tar.gz
Algorithm Hash digest
SHA256 e73bdba4114db4bf14dba945eb03b145c5282d2bd7aaf26c924934e165fb7804
MD5 04e0f169220dad75c73e7c5a8606f424
BLAKE2b-256 9691478ff5c196628a670e7fc0bea14d2f4cccd9ca7d9ef5a50dd72524e77a5f

See more details on using hashes here.

Provenance

The following attestation bundles were made for cc_auth-0.1.0.tar.gz:

Publisher: publish.yml on computacenter-ro/cc-auth

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cc_auth-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: cc_auth-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 13.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cc_auth-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 952cea4cf9d72bb925645492580ece75d1c4e586611937a2b321cd650c6ce0fb
MD5 a3990e3ea54545f30870820028cb6725
BLAKE2b-256 1e31ed58bf1f8b8307309acee23a684634b105cac1d13d2cc0f8bbce291ee989

See more details on using hashes here.

Provenance

The following attestation bundles were made for cc_auth-0.1.0-py3-none-any.whl:

Publisher: publish.yml on computacenter-ro/cc-auth

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page