Metadata-Version: 2.5
Name: django-channels-jwt-stateless
Version: 0.1.2
Summary: Stateless JWT authentication middleware for Django Channels. Zero database queries on WebSocket handshake.
Project-URL: Homepage, https://github.com/zxzinn/django-channels-jwt-stateless
Project-URL: Repository, https://github.com/zxzinn/django-channels-jwt-stateless
Project-URL: Issues, https://github.com/zxzinn/django-channels-jwt-stateless/issues
Author: zxzinn
License-Expression: MIT
License-File: LICENSE
Keywords: authentication,channels,django,jwt,stateless,websocket
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
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: Topic :: Internet :: WWW/HTTP
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: channels>=4.0
Requires-Dist: django>=4.2
Requires-Dist: pyjwt>=2.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-django>=4.8; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# django-channels-jwt-stateless

Stateless JWT authentication middleware for Django Channels.
**Zero database queries on WebSocket handshake.**

Existing Django Channels JWT packages call `User.objects.get()` on every
WebSocket connect. Under burst traffic this saturates the connection pool
and causes multi-second stalls. This package verifies the JWT signature
and builds a lightweight `HandshakeUser` from the token claims instead.

## Install

```
pip install django-channels-jwt-stateless
```

## Quick Start

```python
# asgi.py
from channels.routing import ProtocolTypeRouter, URLRouter
from django_channels_jwt_stateless import JWTAuthMiddleware

application = ProtocolTypeRouter({
    "http": django_application,
    "websocket": JWTAuthMiddleware(URLRouter(websocket_urlpatterns)),
})
```

Works out of the box with `djangorestframework-simplejwt` defaults
(HS256, `SECRET_KEY`, `user_id` claim). Pass the token as `?token=<jwt>`
in the WebSocket URL.

### JWTAuthMiddlewareStack

Use `JWTAuthMiddlewareStack` instead only when your consumers need
`scope["session"]` or `scope["cookies"]`. Two things to know:

- The session user never reaches `scope["user"]`: this middleware always
  sets it first, so channels' `AuthMiddleware` only writes into an unread
  attribute. Session-cookie login is not a fallback.
- channels' `get_user()` still runs on every handshake (a
  `database_sync_to_async` hop, plus session and user DB queries for
  clients that carry a `sessionid` cookie), which partly defeats the
  purpose of this package. Prefer the bare middleware.

## `scope["user"]`

On a valid token, `scope["user"]` is a `HandshakeUser` instance:

| Attribute | Value |
|-----------|-------|
| `.id` / `.pk` | From the JWT `user_id` claim |
| `.is_anonymous` | `False` |
| `.is_authenticated` | `True` |

On an invalid, expired, or missing token it is `AnonymousUser`.

`HandshakeUser` is not a Django model instance: it has no `_meta`, so
`channels.auth.login(scope, user)` raises `AttributeError` with it.
Non-integer primary keys (e.g. UUID) arrive as strings, exactly as the
JWT claim stores them; avoid `==` comparisons against `uuid.UUID`.

## Configuration

Optional. Add to `settings.py`:

```python
CHANNELS_JWT_STATELESS = {
    "ALGORITHM": "HS256",              # default
    "SIGNING_KEY": None,               # defaults to SECRET_KEY
    "USER_ID_CLAIM": "user_id",        # default
    "TOKEN_QUERY_PARAM": "token",      # default
    "IS_BLACKLISTED": None,            # dotted path to (jti) -> bool
    "TOKEN_DECODER": None,             # dotted path to (token) -> dict
    "SUBPROTOCOL_PREFIX": None,        # e.g. "access_token"
}
```

`TOKEN_DECODER` and `IS_BLACKLISTED` accept both sync and async callables.

## Token Blacklisting

```python
# myapp/auth.py
from django.core.cache import cache

async def is_blacklisted(jti: str) -> bool:
    return await cache.aget(f"blacklist:{jti}") is not None

# settings.py
CHANNELS_JWT_STATELESS = {
    "IS_BLACKLISTED": "myapp.auth.is_blacklisted",
}
```

## Subprotocol Auth

To avoid leaking tokens in query strings:

```python
CHANNELS_JWT_STATELESS = {"SUBPROTOCOL_PREFIX": "access_token"}
```

Client sends `Sec-WebSocket-Protocol: access_token.<jwt>`.

## Compatibility

Python 3.10+, Django 4.2+, Channels 4+, PyJWT 2+.

## License

MIT
