Skip to content

Commit 9ffe16c

Browse files
committed
Update README.md
1 parent a01356c commit 9ffe16c

File tree

1 file changed

+289
-6
lines changed

1 file changed

+289
-6
lines changed

README.md

Lines changed: 289 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ This package provides integration with the following Cloudflare products:
1515
* **Cloudflare Workers**: Run your Django application on Cloudflare's serverless compute platform.
1616
* **Cloudflare D1**: Use Cloudflare's serverless SQL database as a backend for your Django application.
1717
* **Cloudflare Durable Objects**: Leverage stateful objects for applications requiring persistent state within Cloudflare Workers.
18+
* **Cloudflare Access Authentication**: Seamless authentication middleware for Cloudflare Access protected applications.
1819

19-
### 1. Cloudflare Workers Integration
20+
## Cloudflare Workers Integration
2021

2122
Run your Django application directly on Cloudflare Workers for edge performance and scalability.
2223

@@ -174,7 +175,7 @@ root
174175
Access `https://your-worker-url.com/__run_migrations__/` to trigger migrations.
175176
Access `https://your-worker-url.com/__create_admin__/` to trigger admin creation.
176177
177-
### 2. Cloudflare D1 Integration
178+
## Cloudflare D1 Integration
178179
179180
Use Cloudflare D1, a serverless SQL database, as your Django application's database.
180181
@@ -184,7 +185,7 @@ Use Cloudflare D1, a serverless SQL database, as your Django application's datab
184185
185186
There are two ways to connect to D1:
186187
187-
#### a) D1 Binding (Recommended for Workers)
188+
### a) D1 Binding (Recommended for Workers)
188189
189190
When your Django application is running on Cloudflare Workers, use D1 Bindings for the fastest access. The D1 database is made available to your worker via an environment binding.
190191
@@ -200,7 +201,7 @@ DATABASES = {
200201
```
201202
Ensure your `wrangler.toml` includes the D1 database binding (as shown in the Cloudflare Workers section).
202203

203-
#### b) D1 API (For Local Development or External Access)
204+
### b) D1 API (For Local Development or External Access)
204205

205206
Connect to D1 via its HTTP API. This method is suitable for local development (e.g., running migrations) or when accessing D1 from outside Cloudflare Workers.
206207

@@ -220,7 +221,7 @@ DATABASES = {
220221

221222
A simple tutorial for getting started with Django and D1 is [available here](https://massadas.com/posts/django-meets-cloudflare-d1/).
222223

223-
### 3. Cloudflare Durable Objects Integration
224+
## Cloudflare Durable Objects Integration
224225

225226
Utilize Durable Objects for stateful data persistence directly within your Cloudflare Workers. This is useful for applications requiring low-latency access to state associated with specific objects or instances.
226227

@@ -276,6 +277,278 @@ Utilize Durable Objects for stateful data persistence directly within your Cloud
276277
```
277278
The `django_cf.do_binding` engine will automatically use the Durable Object binding specified in your `wrangler.toml` that is associated with the `DjangoCFDurableObject` class. Ensure your `wrangler.toml` correctly binds a name to your `DjangoCFDurableObject` subclass.
278279

280+
## Cloudflare Access Authentication
281+
282+
Seamless authentication middleware for Cloudflare Access protected applications. The middleware is implemented using only Python standard library modules - no external dependencies required!
283+
284+
### Quick Start
285+
286+
#### 1. Add to INSTALLED_APPS
287+
288+
```python
289+
# settings.py
290+
INSTALLED_APPS = [
291+
# ... your other apps
292+
'django_cf',
293+
]
294+
```
295+
296+
#### 2. Configure Cloudflare Access Middleware
297+
298+
Add the middleware to your Django settings:
299+
300+
```python
301+
# settings.py
302+
MIDDLEWARE = [
303+
'django.middleware.security.SecurityMiddleware',
304+
'django.contrib.sessions.middleware.SessionMiddleware', # Must be before CloudflareAccessMiddleware
305+
'django.middleware.common.CommonMiddleware',
306+
'django.middleware.csrf.CsrfViewMiddleware',
307+
'django_cf.middleware.CloudflareAccessMiddleware',
308+
# ... other middleware (should be placed appropriately in your middleware stack)
309+
]
310+
```
311+
312+
#### 3. Configure Cloudflare Access Settings
313+
314+
Add the required settings to your Django configuration:
315+
316+
```python
317+
# settings.py
318+
319+
# Required settings (at least one must be provided)
320+
CLOUDFLARE_ACCESS_AUD = 'your-application-audience-tag' # Optional if team name is provided
321+
CLOUDFLARE_ACCESS_TEAM_NAME = 'yourteam' # Optional if AUD is provided
322+
323+
# Optional settings
324+
CLOUDFLARE_ACCESS_EXEMPT_PATHS = [
325+
'/health/',
326+
'/api/public/',
327+
'/admin/login/', # If you want to keep Django admin login
328+
]
329+
CLOUDFLARE_ACCESS_CACHE_TIMEOUT = 3600 # Cache timeout for public keys (seconds)
330+
```
331+
332+
### Required Settings (At Least One)
333+
334+
You must provide **at least one** of the following settings:
335+
336+
#### `CLOUDFLARE_ACCESS_AUD`
337+
Your Cloudflare Access Application Audience (AUD) tag. You can find this in your Cloudflare Zero Trust dashboard:
338+
1. Go to Access → Applications
339+
2. Select your application
340+
3. Copy the "Application Audience (AUD) Tag"
341+
342+
**When to use**: Provide this if you want strict audience validation or if you have multiple applications with the same team.
343+
344+
#### `CLOUDFLARE_ACCESS_TEAM_NAME`
345+
Your Cloudflare team name (the subdomain part of your team domain). For example, if your team domain is `mycompany.cloudflareaccess.com`, then your team name is `mycompany`.
346+
347+
**When to use**: Provide this if you want simpler configuration or if you only have one application per team.
348+
349+
### Configuration Options
350+
351+
You can configure the middleware in three ways:
352+
353+
1. **Both AUD and Team Name** (Recommended for production):
354+
```python
355+
CLOUDFLARE_ACCESS_AUD = 'your-application-audience-tag'
356+
CLOUDFLARE_ACCESS_TEAM_NAME = 'yourteam'
357+
```
358+
359+
2. **Only AUD** (Team name extracted from JWT):
360+
```python
361+
CLOUDFLARE_ACCESS_AUD = 'your-application-audience-tag'
362+
```
363+
364+
3. **Only Team Name** (AUD validated but not enforced):
365+
```python
366+
CLOUDFLARE_ACCESS_TEAM_NAME = 'yourteam'
367+
```
368+
369+
### Optional Settings
370+
371+
#### `CLOUDFLARE_ACCESS_EXEMPT_PATHS`
372+
Default: `[]`
373+
374+
List of URL paths that should be exempt from Cloudflare Access authentication. Useful for:
375+
- Health check endpoints
376+
- Public API endpoints
377+
- Webhooks
378+
- Static file serving (if not handled by your web server)
379+
380+
Example:
381+
```python
382+
CLOUDFLARE_ACCESS_EXEMPT_PATHS = [
383+
'/health/',
384+
'/api/public/',
385+
'/webhooks/',
386+
'/static/', # If serving static files through Django
387+
]
388+
```
389+
390+
#### `CLOUDFLARE_ACCESS_CACHE_TIMEOUT`
391+
Default: `3600` (1 hour)
392+
393+
How long to cache Cloudflare's public keys (in seconds). Cloudflare rotates these keys periodically, so caching reduces API calls while ensuring fresh keys are fetched when needed.
394+
395+
### How It Works
396+
397+
#### Authentication Flow
398+
399+
1. **Request Processing**: For each incoming request, the middleware checks if the path is exempt from authentication
400+
2. **JWT Extraction**: Extracts the JWT token from:
401+
- `CF-Access-Jwt-Assertion` header (preferred)
402+
- `CF_Authorization` cookie (fallback)
403+
3. **Team Discovery**: If team name is not configured, extracts it from the JWT's issuer claim
404+
4. **Public Key Retrieval**: Fetches Cloudflare's public keys from the team's certs endpoint
405+
5. **Token Validation**: Validates the JWT against Cloudflare's public keys:
406+
- Validates token signature and expiration
407+
- Validates audience (AUD) if configured
408+
6. **User Management**:
409+
- Extracts email and name from JWT claims
410+
- Creates new Django user if doesn't exist
411+
- Updates existing user's name if changed
412+
- Logs the user into Django session
413+
7. **Response**: Proceeds with the request if authentication succeeds, returns 401 if it fails
414+
415+
#### User Creation
416+
417+
When a user authenticates for the first time:
418+
- Username is set to the user's email address
419+
- Email is extracted from JWT `email` claim
420+
- Name is split into first_name and last_name from JWT `name` claim
421+
- User is created with `is_active=True`
422+
423+
For existing users:
424+
- Name is updated if it has changed in Cloudflare
425+
- User is automatically logged in
426+
427+
### Security Considerations
428+
429+
#### Token Validation
430+
- All JWT tokens are validated against Cloudflare's public keys
431+
- Tokens are checked for expiration
432+
- Audience (AUD) is validated to ensure tokens are for your application
433+
434+
#### Caching
435+
- Public keys are cached to reduce API calls to Cloudflare
436+
- Cache keys are scoped to your team name
437+
- Failed key fetches are logged but don't crash the application
438+
439+
#### Error Handling
440+
- Authentication failures return 401 responses
441+
- Server errors return 500 responses
442+
- All errors are logged for debugging
443+
444+
### Logging
445+
446+
The middleware uses Django's logging system with logger name `django_cf.middleware`. To see debug information:
447+
448+
```python
449+
# settings.py
450+
LOGGING = {
451+
'version': 1,
452+
'disable_existing_loggers': False,
453+
'handlers': {
454+
'console': {
455+
'class': 'logging.StreamHandler',
456+
},
457+
},
458+
'loggers': {
459+
'django_cf.middleware': {
460+
'handlers': ['console'],
461+
'level': 'INFO',
462+
'propagate': True,
463+
},
464+
},
465+
}
466+
```
467+
468+
### Common Issues and Troubleshooting
469+
470+
#### 401 Unauthorized Responses
471+
472+
**Cause**: JWT token is missing, invalid, or expired.
473+
474+
**Solutions**:
475+
1. Ensure your application is behind Cloudflare Access
476+
2. Check that users are properly authenticated with Cloudflare Access
477+
3. If using `CLOUDFLARE_ACCESS_AUD`, verify it matches your application's AUD tag
478+
4. If using `CLOUDFLARE_ACCESS_TEAM_NAME`, check that it's correct
479+
5. Review middleware logs for specific validation errors
480+
481+
#### 500 Internal Server Error
482+
483+
**Cause**: Unable to fetch Cloudflare public keys or other configuration issues.
484+
485+
**Solutions**:
486+
1. Verify your team name is correct (if using `CLOUDFLARE_ACCESS_TEAM_NAME`)
487+
2. Check network connectivity to Cloudflare
488+
3. Review logs for specific error messages
489+
4. If using only AUD, ensure the JWT contains a valid issuer claim
490+
491+
#### Users Not Being Created
492+
493+
**Cause**: Missing email claim in JWT or database issues.
494+
495+
**Solutions**:
496+
1. Check that your Cloudflare Access application is configured to include email in JWT
497+
2. Verify Django database migrations are up to date
498+
3. Check Django user model permissions
499+
500+
### Advanced Usage
501+
502+
#### Custom User Creation
503+
504+
If you need custom user creation logic, you can subclass the middleware:
505+
506+
```python
507+
from django_cf.middleware import CloudflareAccessMiddleware
508+
from django.contrib.auth import get_user_model
509+
510+
User = get_user_model()
511+
512+
class CustomCloudflareAccessMiddleware(CloudflareAccessMiddleware):
513+
def _get_or_create_user(self, email, name):
514+
# Your custom user creation logic here
515+
user, created = User.objects.get_or_create(
516+
email=email,
517+
defaults={
518+
'username': email,
519+
'first_name': name.split(' ')[0] if name else '',
520+
'last_name': ' '.join(name.split(' ')[1:]) if name else '',
521+
'is_active': True,
522+
'is_staff': email.endswith('@yourcompany.com'), # Custom logic
523+
}
524+
)
525+
return user
526+
```
527+
528+
#### Multiple Applications
529+
530+
If you have multiple Cloudflare Access applications, you can configure different middleware instances or use environment-specific settings.
531+
532+
### Development
533+
534+
#### Testing
535+
536+
When developing applications with this middleware, you may want to disable it in certain environments:
537+
538+
```python
539+
# settings.py
540+
if DEBUG and not os.getenv('ENABLE_CF_ACCESS'):
541+
# Remove CloudflareAccessMiddleware from MIDDLEWARE list
542+
MIDDLEWARE = [m for m in MIDDLEWARE if 'CloudflareAccessMiddleware' not in m]
543+
```
544+
545+
#### Local Development
546+
547+
For local development without Cloudflare Access:
548+
1. Set exempt paths to include your development URLs
549+
2. Use Django's built-in authentication for local testing
550+
3. Consider using environment variables to toggle the middleware
551+
279552
## Limitations
280553

281554
* **D1 Database:**
@@ -285,13 +558,23 @@ Utilize Durable Objects for stateful data persistence directly within your Cloud
285558
* Always refer to the official [Django limitations for SQLite databases](https://docs.djangoproject.com/en/stable/ref/databases/#sqlite-notes), as D1 is SQLite-compatible but has its own serverless characteristics.
286559
* **Durable Objects:** While powerful for stateful serverless applications, ensure you understand the consistency model and potential data storage costs associated with Durable Objects.
287560

288-
## Development
561+
## Contributing
289562

290563
See [DEVELOPMENT.md](DEVELOPMENT.md) for details on setting up a development environment and contributing to `django-cf`.
291564

565+
We welcome contributions! Please see our contributing guidelines and feel free to submit issues and pull requests.
566+
567+
## Support
568+
569+
For issues and questions:
570+
1. Check the troubleshooting section above
571+
2. Review Cloudflare Access documentation
572+
3. Submit an issue on GitHub with detailed error logs and configuration (remove sensitive information)
573+
292574
## License
293575

294576
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
577+
295578
---
296579

297580
*For a more detailed example of a Django project structured for Cloudflare Workers with D1, check out relevant community projects or the `django-cf` examples if available.*

0 commit comments

Comments
 (0)