Metadata-Version: 2.4
Name: django-tls-certificate-auth
Version: 1.1.0
Summary: Django authentication middleware using TLS client certificates
Author-Email: Mark Parncutt <me@markparncutt.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Django :: 6.1
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
Project-URL: Repository, https://github.com/U-238/django-tls-certificate-auth
Project-URL: Issues, https://github.com/U-238/django-tls-certificate-auth/issues
Requires-Python: >=3.10
Requires-Dist: Django>=5.2
Description-Content-Type: text/markdown

# django-tls-certificate-auth

Authenticate Django users using TLS client certificates terminated by a reverse proxy (for example, nginx).

The package reads certificate details from request headers and logs users in when they visit `settings.LOGIN_URL`. Requests without a certificate reach your
normal login view, allowing password authentication when configured. nginx must
use `ssl_verify_client optional` if clients without certificates should be allowed
through the proxy. An attempted certificate login that fails returns HTTP 403.

## Installation

Supports Django 5.2, 6.0, and 6.1 on Python 3.10–3.14. Django 6.x requires
Python 3.12 or newer.

Install from PyPI:

```bash
pip install django-tls-certificate-auth
```

## Django Setup

### 1. Add middleware

Add the middleware to `MIDDLEWARE` (after session middleware so `login()` can write to the session):

```python
MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    # ...
    "django_tls_certificate_auth.TLSClientCertAuthenticationMiddleware",
    # ...
]
```

### 2. Add authentication backend

Add the backend to `AUTHENTICATION_BACKENDS`:

```python
AUTHENTICATION_BACKENDS = [
    "django_tls_certificate_auth.TLSClientCertAuthenticationBackend",
]
```

If you still want username/password (or other methods), include Django's default backend as well:

```python
AUTHENTICATION_BACKENDS = [
    "django_tls_certificate_auth.TLSClientCertAuthenticationBackend",
    "django.contrib.auth.backends.ModelBackend",
]
```

### 3. Configure login URLs

The middleware runs only when the request path matches `LOGIN_URL`.

```python
LOGIN_URL = "/accounts/login/"
LOGIN_REDIRECT_URL = "/"
```

When a valid certificate is present, the middleware authenticates the user and redirects to `LOGIN_REDIRECT_URL`.

## Required Proxy Headers

Your reverse proxy must verify client certificates and pass these headers to Django:

- `X-Client-Cert-Valid`: must be `SUCCESS`
- `X-Client-Cert-Dn`: certificate subject DN string containing `CN=<username>`

nginx example:

```nginx
ssl_client_certificate /etc/ssl/certs/mysite.ca.crt;
ssl_verify_client on;

proxy_set_header X-Client-Cert-Valid $ssl_client_verify;
proxy_set_header X-Client-Cert-Dn $ssl_client_s_dn;
```

## How Username Mapping Works

The backend extracts `CN` from `X-Client-Cert-Dn` and looks up a Django user by your user model's `USERNAME_FIELD`.

Example DN:

```text
/C=AU/O=Example/CN=alice
```

This maps to username `alice`. Both comma-separated proxy subjects and OpenSSL's
leading-slash format are supported, including escaped separators and UTF-8 hex
escapes. Exactly one nonempty `CN` (or its OID `2.5.4.3`) is required. Duplicate
common names, malformed escapes, control characters, quoted legacy values, and
binary/BER values are rejected. Common names are case-sensitive user identities.

Inactive users are rejected, matching Django's default authentication policy.

## Package Settings

The following settings can be set in your Django project's `settings.py`:

### `TLS_CLIENT_AUTHENTICATION_AUTO_CREATE`

- Type: `bool`
- Default: `False`
- Behavior:
  - `False`: if the mapped user does not exist, the backend returns `None`; the middleware returns HTTP 403.
  - `True`: if the mapped user does not exist, the backend creates it using `create_user(...)`.

Example:

```python
TLS_CLIENT_AUTHENTICATION_AUTO_CREATE = True
```

### `TLS_CLIENT_AUTHENTICATION_USER_DEFAULTS`

- Type: `dict`
- Default: `{}`
- Extra keyword arguments passed to your user manager's `create_user(...)` when
  auto-creation is enabled. Supply any fields required by your custom manager.
  Neither `USERNAME_FIELD` nor `password` may appear here; the certificate defines
  the identity and new accounts receive an unusable password.

For a custom user model requiring a display name:

```python
TLS_CLIENT_AUTHENTICATION_USER_DEFAULTS = {"display_name": "Certificate user"}
```

An incomplete configuration raises `ImproperlyConfigured`. Concurrent creation
of the same unique identity reuses the account created by the winning request.
Your user model must enforce uniqueness of `USERNAME_FIELD` in the database.

### `TLS_CLIENT_AUTHENTICATION_DEBUG_FAKE_HEADERS`

- Type: `dict`
- Default: `{}`
- Behavior:
  - Any headers defined in this dictionary (with "HTTP_" prefix) will be treated by this authentication middleware as if they had been sent as HTTP headers.
  - This setting is ignored unless `DEBUG` is set to true. It is intended to simplify the use of this library during development.

Example:

```python
TLS_CLIENT_AUTHENTICATION_DEBUG_FAKE_HEADERS = {
    "HTTP_X_CLIENT_CERT_VALID": "SUCCESS",
    "HTTP_X_CLIENT_CERT_DN": "C=AU, ST=Victoria, L=Melbourne, CN=mp"
}
```

## Django Settings Used by This Package

These are standard Django settings consumed by the package:

- `LOGIN_URL`: path that triggers certificate authentication
- `LOGIN_REDIRECT_URL`: destination after successful TLS login

## Notes

- Header names and success value are currently fixed in code:
  - `X-Client-Cert-Valid` must equal `SUCCESS`
  - `X-Client-Cert-Dn` must include a `CN=...` component
- Ensure your proxy strips/overwrites client-supplied versions of these headers so users cannot spoof certificate identity.
- Restrict direct access to the Django application so requests must pass through
  the trusted proxy.

## Development

Install the package in a virtual environment and run the tests:

```bash
pip install -e .
python -W error -m django test tests --settings=tests.settings
python -W error -m django test tests.custom_users --settings=tests.custom_settings
```

## Releases

Commit the release changes and tag that clean commit using `X.Y.Z` or `vX.Y.Z`.
Prerelease tags use standard Python suffixes such as `1.2.3rc1`, `1.2.3a1`, or
`1.2.3b1`; the old `pre` tag convention is no longer accepted.

The publish workflow verifies that the checkout is clean, the tag points to
`HEAD`, and the wheel version matches the tag without a local suffix. It tests
the built wheel with default and custom user models before uploading. Configure
GitHub trusted publishers for both the `testpypi` and `pypi` environments. Every
release goes to TestPyPI first; stable releases go to PyPI only after that upload
succeeds. Prereleases remain on TestPyPI.

You can check a release locally after installing `build` and `twine`:

```bash
python scripts/check_release.py 1.2.3
python -m build --outdir /tmp/tls-auth-release-1.2.3
python scripts/check_release.py 1.2.3 --dist /tmp/tls-auth-release-1.2.3
python -m twine check /tmp/tls-auth-release-1.2.3/*
python -m unittest discover -s scripts -p 'test_*.py'
```

Use an empty output directory for each build to avoid uploading stale artifacts.
