Metadata-Version: 2.5
Name: django-clerk-users
Version: 0.4.1
Summary: Integrate Clerk with Django
Project-URL: Changelog, https://github.com/jmitchel3/django-clerk-users/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/jmitchel3/django-clerk-users
Project-URL: Issues, https://github.com/jmitchel3/django-clerk-users/issues
Project-URL: Repository, https://github.com/jmitchel3/django-clerk-users
Author: Justin Mitchel
License-Expression: MIT
License-File: LICENSE
License-File: NOTICE
Keywords: authentication,clerk,django,jwt,webhooks
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.12
Requires-Dist: cryptography>=45
Requires-Dist: django<7,>=4.2
Requires-Dist: httpx>=0.27
Requires-Dist: pyjwt>=2.8
Requires-Dist: svix>=1
Provides-Extra: drf
Requires-Dist: djangorestframework>=3.14; extra == 'drf'
Provides-Extra: sdk
Requires-Dist: clerk-backend-api>=6.0.1; extra == 'sdk'
Description-Content-Type: text/markdown

# Django Clerk Users

Integrate [Clerk](https://clerk.com) authentication with Django.

> **Production note:** Pin versions and review release notes before upgrading
> between minor versions.

## Features

- Custom user model (`ClerkUser`) with Clerk integration
- JWT token validation via Clerk SDK
- Session-based authentication middleware (validates once, caches in session)
- Webhook handling with Svix signature verification
- Optional organizations support (separate sub-app)
- Django REST Framework authentication (optional)
- Server-side Clerk SDK helpers for account provisioning and invite links

## Installation

```bash
pip install django-clerk-users
```

For Django REST Framework support:

```bash
pip install django-clerk-users[drf]
```

To use the official `clerk-backend-api` SDK for server-side API calls instead
of the built-in thin client (note that this reintroduces that SDK's
`cryptography<49` pin):

```bash
pip install django-clerk-users[sdk]
```

Use `.env.example` as a starting point for required Clerk settings in each
environment.

## Compatibility

This package supports Python 3.12 through 3.14 and Django 4.2, 5.2, and 6.0.
CI installs and tests the built wheel across those supported Django/Python
combinations and exercises the optional Django REST Framework extra.

`cryptography` is declared as `>=45` with **no upper bound**, and a default
install now has no ceiling in practice either. `clerk-backend-api` pins
`cryptography<49`, and that pin is what resolvers obey, so it lives in the
optional `[sdk]` extra rather than the base dependencies:

```bash
pip install django-clerk-users          # cryptography 50.x
pip install django-clerk-users[sdk]     # held to cryptography 48.x
```

A scheduled workflow reports where the upstream ceiling sits, and a
`py313-cryptolatest` tox environment runs the suite against the newest
`cryptography`:

```bash
uv run python scripts/check_cryptography_ceiling.py
uv run tox -e py313-cryptolatest
```

### Clerk API client backends

Server-side Clerk API calls go through a client selected by
`CLERK_CLIENT_BACKEND`:

| Value | Client | Requires | `cryptography` |
|---|---|---|---|
| `"thin"` *(default)* | Built-in REST client | nothing extra | unbounded |
| `"sdk"` | Official `clerk-backend-api` | `[sdk]` extra | capped at `<49` |

```python
# settings.py — only needed to opt into the official SDK
CLERK_CLIENT_BACKEND = "sdk"
```

Selection is **by setting only, never by what happens to be installed**.
`get_clerk_client()` is public API, so picking an implementation based on
importability would make response models, error types, retries, and available
methods depend on the environment, and would silently restore the
`cryptography` ceiling for anyone who acquired `clerk-backend-api` as a
transitive dependency. Setting `CLERK_CLIENT_BACKEND = "sdk"` without
installing the extra raises `ClerkConfigurationError` rather than falling back.

Session token verification never uses the SDK on either backend.

## Quick Start

### 1. Add to installed apps

```python
INSTALLED_APPS = [
    # ...
    "django_clerk_users",
    # Optional: for organization support
    # "django_clerk_users.organizations",
]
```

### 2. Configure settings

```python
# Required
CLERK_SECRET_KEY = "sk_live_..."  # From Clerk Dashboard
CLERK_WEBHOOK_SIGNING_KEY = "whsec_..."  # From Clerk Webhooks
CLERK_FRONTEND_HOSTS = ["https://your-app.com"]  # Your frontend URLs

# Optional
CLERK_SESSION_REVALIDATION_SECONDS = 300  # Re-validate JWT every 5 minutes
CLERK_CACHE_TIMEOUT = 300  # Cache timeout for user lookups
```

### 3. Set the user model

```python
AUTH_USER_MODEL = "django_clerk_users.ClerkUser"
```

Or extend the abstract model for custom fields:

```python
# myapp/models.py
from django.db import models
from django_clerk_users.models import AbstractClerkUser


class CustomUser(AbstractClerkUser):
    company = models.CharField(max_length=255, blank=True)

    class Meta(AbstractClerkUser.Meta):
        swappable = "AUTH_USER_MODEL"


# settings.py
AUTH_USER_MODEL = "myapp.CustomUser"
```

### 4. Add middleware

```python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django_clerk_users.middleware.ClerkAuthMiddleware",  # Add after AuthenticationMiddleware
    # ...
]
```

### 5. Add authentication backend

**For Clerk-only authentication:**

```python
AUTHENTICATION_BACKENDS = [
    "django_clerk_users.authentication.ClerkBackend",
]
```

**For hybrid authentication (Clerk + Django admin):**

If you want to support both Clerk authentication (JWT) and traditional Django admin login (username/password), use both backends:

```python
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",  # For Django admin
    "django_clerk_users.authentication.ClerkBackend",  # For Clerk JWT
]
```

This allows:
- Admin users to log in via Django admin with username/password
- Frontend users to authenticate via Clerk JWT tokens
- The middleware automatically detects which authentication method was used

### 6. Run migrations

```bash
python manage.py migrate
```

### 7. Configure webhooks

Add the webhook URL to your `urls.py`:

```python
from django_clerk_users.webhooks import clerk_webhook_view

urlpatterns = [
    # ...
    path("webhooks/clerk/", clerk_webhook_view, name="clerk_webhook"),
]
```

Then configure your Clerk Dashboard to send webhooks to `https://your-app.com/webhooks/clerk/`.

For additional Clerk webhook endpoints, each endpoint gets its own Svix signing
secret. Use the package verifier with an endpoint-specific setting:

```python
from django.http import JsonResponse
from django_clerk_users.webhooks import clerk_webhook_required


@clerk_webhook_required(signing_key_setting="CLERK_ACTIVATION_WEBHOOK_SIGNING_KEY")
def activation_webhook(request):
    data = request.clerk_webhook_data
    return JsonResponse({"ok": True, "type": data.get("type")})
```

### 8. Create admin users (for hybrid authentication)

If you're using hybrid authentication, create an admin user for Django admin access:

```bash
python manage.py createsuperuser
```

This creates a user with:
- Username/password authentication (for Django admin)
- No `clerk_id` (since they're not Clerk users)
- Access to Django admin panel

Note: Regular Clerk users are created automatically via webhooks when they sign up through your frontend.

## Usage

### Accessing the user in views

```python
def my_view(request):
    if request.user.is_authenticated:
        # Access Clerk user attributes
        print(request.user.clerk_id)
        print(request.user.email)
        print(request.user.full_name)

        # Access organization (if using organizations)
        print(request.org)  # Organization ID from JWT
```

### Decorators

```python
from django_clerk_users.decorators import clerk_user_required


@clerk_user_required
def protected_view(request):
    # Only authenticated Clerk users can access
    return HttpResponse(f"Hello, {request.user.email}")
```

### Django REST Framework

```python
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "django_clerk_users.authentication.ClerkAuthentication",
    ],
}
```

For hybrid APIs that accept Clerk bearer tokens and Django session users, use
the combined authenticator:

```python
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "django_clerk_users.authentication.ClerkSessionAuthentication",
    ],
}
```

`ClerkSessionAuthentication` checks `Authorization: Bearer ...` with Clerk first.
Requests without a bearer token fall back to Django session authentication.
Bearer token failures are not hidden by the session fallback.

### Server-side Clerk operations

For backend flows that create accounts outside Clerk's hosted sign-up UI, use the
server API helpers instead of hand-writing Clerk SDK calls in each Django app:

```python
from django_clerk_users.server_api import provision_clerk_user_access_link


result = provision_clerk_user_access_link(
    "invitee@example.com",
    "https://app.example.com/sign-in",
    first_name="Ada",
    public_metadata={"invite_type": "staff"},
    expires_in_seconds=7 * 24 * 3600,
    auto_username=True,  # useful when the Clerk instance requires usernames
)

if result["access_link"]:
    send_invite_email(result["access_link"])
```

The helpers cover common server-side tasks:

- `create_clerk_user()` creates passwordless or password-backed Clerk users.
- `create_clerk_sign_in_token()` and `create_clerk_sign_in_link()` mint one-time
  access links using Clerk's `__clerk_ticket` flow.
- `get_clerk_user_by_email()`, `set_clerk_user_email()`, and
  `update_clerk_user_public_metadata()` keep Django profile workflows in sync
  with Clerk.
- `revoke_clerk_user_sessions()`, `send_clerk_invitation()`, and
  `revoke_clerk_invitation()` wrap common account-management operations.

When `CLERK_SECRET_KEY` is missing or set to a documented local placeholder
(`abc123`, `sk_test_mock_secret_key`, or `sk_live_replace_me`), creation helpers
return `{"no_key": True}` and mutation helpers no-op with a falsey result.

## Hybrid Authentication (Clerk + Django Admin)

The package supports hybrid authentication, allowing you to use both Clerk (JWT-based) authentication for your frontend users and traditional Django admin authentication for internal staff.

### How it works

1. **Frontend users**: Authenticate via Clerk JWT tokens (handled by `ClerkAuthMiddleware`)
2. **Admin users**: Authenticate via username/password (handled by Django's `ModelBackend`)
3. The middleware automatically detects which authentication method was used and respects existing sessions

### Configuration

```python
# settings.py
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",  # For Django admin
    "django_clerk_users.authentication.ClerkBackend",  # For Clerk JWT
]
```

### Creating admin users

Admin users don't need a `clerk_id` (it's optional in hybrid mode):

```bash
python manage.py createsuperuser
# Email: admin@example.com
# Password: ********
```

This creates a user with:
- Username/password authentication (no Clerk integration)
- Access to Django admin panel at `/admin/`
- Standard Django permissions (is_staff, is_superuser)

### Session handling

- **Django admin sessions**: Traditional session cookies (set by Django's auth system)
- **Clerk sessions**: JWT validated once, then cached in session with `last_clerk_check` marker
- The middleware checks for `last_clerk_check` to distinguish between the two types

### Use cases

This is particularly useful when:
- Your admin panel is on a different domain than your frontend
- You want internal staff to access Django admin without Clerk accounts
- You need traditional Django auth features (permissions, groups, etc.)
- You're migrating from Django auth to Clerk gradually

### Claim and conversion flows

Some Django apps pre-create users before a Clerk signup exists. For example, a
student, invitee, or imported account may later claim a real Clerk identity. If
Clerk signs the person in before your app finishes the claim, the standard user
sync can create a fresh duplicate user for the claimed email.

Use `absorb_clerk_user_duplicate()` at the point where your app has verified the
claim and knows the historical target user:

```python
from django_clerk_users.utils import absorb_clerk_user_duplicate


def duplicate_is_fresh_shell(user):
    return (
        not user.is_staff
        and not user.is_superuser
        and not user.has_usable_password()
        # Add app-specific checks here, e.g. no memberships, roles, orders, etc.
    )


absorb_clerk_user_duplicate(
    target_user,
    email=claimed_email,
    safe_to_delete=duplicate_is_fresh_shell,
)
target_user.email = claimed_email
target_user.save(update_fields=["email"])
```

The helper moves the duplicate's `clerk_id` to the target user, deletes the
duplicate by default, and invalidates affected Clerk user cache entries. It raises
`ClerkUserMergeConflictError` instead of deleting when the duplicate is not safe
to absorb.

## Organizations (Optional)

For Clerk organization support:

```python
# settings.py
INSTALLED_APPS = [
    # ...
    "django_clerk_users",
    "django_clerk_users.organizations",
]

MIDDLEWARE = [
    # ...
    "django_clerk_users.middleware.ClerkAuthMiddleware",
    "django_clerk_users.organizations.middleware.ClerkOrganizationMiddleware",
]
```

## Management Commands

```bash
# Sync users from Clerk
python manage.py sync_clerk_users

# Sync organizations from Clerk
python manage.py sync_clerk_organizations

# Preview migration of existing Django users into Clerk
python manage.py migrate_users_to_clerk --source-model auth.User --all --dry-run --skip-existing

# Migrate a bounded batch and link local rows when matching Clerk users already exist
python manage.py migrate_users_to_clerk --source-model auth.User --all --limit 100 --skip-existing
```

`migrate_users_to_clerk` creates passwordless Clerk users because Django password
hashes cannot be migrated into Clerk. Existing local users with a `clerk_id`
field are linked to matching Clerk users when `--skip-existing` is used, and the
command recovers duplicate-email create races by looking up and linking the
existing Clerk user. `--created-before YYYY-MM-DD` filters on `date_joined` for
classic Django users and `created_at` for `ClerkUser`-based source models.

## Production Checklist

- Set `CLERK_SECRET_KEY`, `CLERK_WEBHOOK_SIGNING_KEY`, and
  `CLERK_FRONTEND_HOSTS` from environment variables or a secret manager.
- Serve webhook endpoints over HTTPS and keep Svix signature verification
  enabled. Use a separate signing secret for each custom webhook endpoint.
- Put `ClerkAuthMiddleware` after Django's `AuthenticationMiddleware`, and put
  `ClerkOrganizationMiddleware` after `ClerkAuthMiddleware` when using
  organizations.
- Run `python manage.py check --deploy` in CI or deployment validation; the
  package registers checks for placeholder secrets, missing frontend host
  allowlists, and middleware ordering.
- Configure application logging for `django_clerk_users.*` so authentication,
  webhook, and sync failures are visible in production. Alert on
  `django_clerk_users.caching` warnings: the package rides out a cache outage,
  but it does so by sending more traffic to Clerk and your database.
- Run `python manage.py migrate` during deploys and run sync commands with
  `--dry-run` before backfilling existing Clerk data.
- Keep the package's CI gates enabled: lockfile checks, Ruff, formatting,
  migrations, tox across supported Python versions, build verification, and
  coverage threshold enforcement.

### Release validation

Before publishing a release, run the artifact and installed-wheel checks:

```bash
uv build
uv run python scripts/check_dist.py
uv run --no-sync python -m pip install --force-reinstall --no-deps dist/django_clerk_users-*.whl
uv run --no-sync python scripts/smoke_installed_wheel.py
```

Then run the read-only live Clerk smoke check against a test/development Clerk
instance:

```bash
export CLERK_SECRET_KEY=sk_test_...
export CLERK_WEBHOOK_SIGNING_KEY=whsec_...
export CLERK_FRONTEND_HOSTS=https://app.example.com
# Optional: make the smoke check verify a known existing user lookup.
export CLERK_LIVE_SMOKE_LOOKUP_EMAIL=user@example.com

uv run python scripts/live_clerk_smoke.py
```

`scripts/live_clerk_smoke.py` performs no writes. It runs Django system checks,
calls Clerk's user list endpoint, optionally looks up an existing user by email,
and verifies a signed Svix webhook payload through the package verifier.

The tag-based release workflow runs these same artifact checks and the live
Clerk smoke check before publishing to PyPI. Configure the `release` environment
with `CLERK_SECRET_KEY` and `CLERK_WEBHOOK_SIGNING_KEY` secrets, plus optional
`CLERK_FRONTEND_HOSTS` and `CLERK_LIVE_SMOKE_LOOKUP_EMAIL` variables.
Use the `Live Clerk Smoke` workflow to run the same installed-wheel and live
Clerk checks manually before tagging a release.

## Auto-Generated Usernames

Clerk doesn't require usernames, but Django often does (for admin, URLs, etc.). This package provides options for generating usernames automatically.

### Synchronous Generation

Generate usernames inline during user creation:

```python
# settings.py
CLERK_AUTO_GENERATE_USERNAME = True  # Enable auto-generation
CLERK_AUTO_GENERATE_USERNAME_PREFIX = "user"  # Optional, default is "user"
```

Usernames are generated as `{prefix}_{uuid8}` (e.g., `user_abc12345`).

### Async Generation (Celery, django-qstash, etc.)

For high-traffic apps, you may want to defer username generation to a background task. Keep `CLERK_AUTO_GENERATE_USERNAME` disabled (the default) and use the `clerk_user_created` signal to trigger your async task:

```python
# myapp/signals.py
from django.dispatch import receiver
from django_clerk_users.webhooks.signals import clerk_user_created


@receiver(clerk_user_created)
def handle_user_created(sender, user, clerk_data, **kwargs):
    from myapp.tasks import generate_username_task

    generate_username_task.delay(user.pk)
```

```python
# myapp/tasks.py (Celery example)
from celery import shared_task
from django_clerk_users.utils import generate_username_for_user


@shared_task
def generate_username_task(user_id: int):
    return generate_username_for_user(user_id)
```

### Backfilling Existing Users

To generate usernames for existing users without one:

```python
from django_clerk_users.utils import generate_usernames_for_users_without

# Synchronous backfill
count = generate_usernames_for_users_without()

# With custom prefix
count = generate_usernames_for_users_without(prefix="member")
```

## Password Sync

When you change a user's password in Django, it can automatically sync to Clerk:

```python
# Sync password to both Django and Clerk (default)
user.set_password("new_password")
user.save()

# Django only - skip Clerk sync
user.set_password("new_password", sync_to_clerk=False)
user.save()
```

Notes:
- Sync is enabled by default (`sync_to_clerk=True`)
- Users without a `clerk_id` (e.g., Django admin users) skip Clerk sync automatically
- Clerk API errors are logged but don't prevent the Django password from being set

Disable global password sync when Django passwords are only for local session
users or staff accounts:

```python
# settings.py
CLERK_SYNC_PASSWORDS = False
```

Existing projects can also use the legacy opt-out flag:

```python
CLERK_DISABLE_PASSWORD_SYNC = True
```

## Caching

The package caches verified JWT payloads, users, and organizations in Django's
default cache. Every one of those caches sits in front of an authoritative
source (Clerk or your database), so the cache is an optimization and never a
source of truth.

A cache backend failure is therefore treated as a cache miss, not as an error.
If your Redis instance is unreachable, rate-limited, or timing out, requests
keep authenticating; they simply re-verify tokens with Clerk and re-query the
database until the cache recovers. Failures are logged to the
`django_clerk_users.caching` logger:

- `WARNING` for a failed read, write, or dedup check.
- `ERROR` for a failed invalidation, because the stale entry is then served
  until it expires on its own (bounded by `CLERK_CACHE_TIMEOUT` and
  `CLERK_ORG_CACHE_TIMEOUT`).

Tracebacks are attached only when that logger is set to `DEBUG`, so a cache
outage does not flood your logs with one traceback per request.

Because a forced miss only ever means "ask the authoritative source again",
this can never cause the package to accept a token or a user it would
otherwise reject.

## Configuration Reference

| Setting | Required | Default | Description |
|---------|----------|---------|-------------|
| `CLERK_SECRET_KEY` | Yes | - | Your Clerk secret key |
| `CLERK_WEBHOOK_SIGNING_KEY` | Yes* | - | Webhook signing secret (*required for webhooks) |
| `CLERK_FRONTEND_HOSTS` | Yes | `[]` | Authorized frontend URLs |
| `CLERK_AUTH_PARTIES` | No | `[]` | Alias for `CLERK_FRONTEND_HOSTS` |
| `CLERK_JWT_KEY` | No | - | PEM public key for networkless token verification (skips the JWKS request) |
| `CLERK_CLIENT_BACKEND` | No | `"thin"` | Clerk API client: `"thin"` (built-in) or `"sdk"` (requires the `[sdk]` extra) |
| `CLERK_SESSION_REVALIDATION_SECONDS` | No | `300` | JWT revalidation interval (seconds) |
| `CLERK_CACHE_TIMEOUT` | No | `300` | User cache timeout (seconds) |
| `CLERK_ORG_CACHE_TIMEOUT` | No | `900` | Organization cache timeout (seconds) |
| `CLERK_API_TIMEOUT_MS` | No | `10000` | Timeout for server-side Clerk SDK helper calls |
| `CLERK_WEBHOOK_DEDUP_TIMEOUT` | No | `45` | Webhook deduplication cache timeout (seconds) |
| `CLERK_AUTO_GENERATE_USERNAME` | No | `False` | Auto-generate usernames synchronously |
| `CLERK_AUTO_GENERATE_USERNAME_PREFIX` | No | `"user"` | Prefix for auto-generated usernames |
| `CLERK_SYNC_PASSWORDS` | No | `True` | Sync Django password changes to Clerk when a user has `clerk_id` |
| `CLERK_DISABLE_PASSWORD_SYNC` | No | `False` | Legacy opt-out; disables password sync when `True` |

Comma-separated strings are accepted for `CLERK_FRONTEND_HOSTS` and
`CLERK_AUTH_PARTIES`. Numeric settings may be provided as strings. Boolean
settings accept common environment values such as `true`, `false`, `1`, and `0`.

## License

MIT

## Contributing

Contributions are welcome! Please open an issue or PR on [GitHub](https://github.com/jmitchel3/django-clerk-users).
