Metadata-Version: 2.4
Name: siwe-django
Version: 0.2.0
Summary: Reusable Django authentication for Sign-In with Ethereum.
Project-URL: Homepage, https://github.com/Quantumlyy/siwe-django
Project-URL: Repository, https://github.com/Quantumlyy/siwe-django
Project-URL: Documentation, https://github.com/Quantumlyy/siwe-django#readme
Author-email: Nejc Drobnic <nejc@ethfollow.xyz>
License-Expression: MIT
License-File: LICENSE
Keywords: Django,EIP-4361,Ethereum,SIWE,authentication
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
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: Topic :: Security
Requires-Python: >=3.10
Requires-Dist: django<6.1,>=5.2
Requires-Dist: eth-utils>=2.2
Requires-Dist: signinwithethereum<6,>=5
Requires-Dist: web3<8,>=7.3
Provides-Extra: cli
Requires-Dist: libcst>=1.4; extra == 'cli'
Requires-Dist: questionary>=2.0; extra == 'cli'
Requires-Dist: rich>=13.7; extra == 'cli'
Requires-Dist: tomlkit>=0.13; extra == 'cli'
Requires-Dist: typer>=0.12; extra == 'cli'
Provides-Extra: drf
Requires-Dist: djangorestframework>=3.16; extra == 'drf'
Provides-Extra: openapi
Requires-Dist: drf-spectacular>=0.27; extra == 'openapi'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Description-Content-Type: text/markdown

# siwe-django

Reusable Django authentication for Sign-In with Ethereum (SIWE / EIP-4361).

`siwe-django` is a reusable Django app: install it into an existing Django
project, mount its URLs where you want them, and keep control of your own UI.
It provides a nonce-based SIWE login flow, session login, wallet linking for
existing Django users, an optional Ethereum-native user model, optional Django
REST Framework views, ENS and Ethereum Identity Kit profile enrichment, and
token-gated Django group sync.

## Ethereum identity stack

`siwe-django` is built to make Django a first-class backend for the best
projects in Ethereum identity:

- [Sign in with Ethereum](https://docs.siwe.xyz/) for wallet authentication.
  The Python side uses the official
  [`signinwithethereum`](https://pypi.org/project/signinwithethereum/)
  distribution from
  [`siwe-py`](https://github.com/signinwithethereum/siwe-py), imported as
  `siwe`.
- [Ethereum Identity Kit](https://ethidentitykit.com/) for SIWE UX, profiles,
  avatars, and social components.
- [Ethereum Follow Protocol](https://efp.app/) for portable onchain social
  graph data and EFP-powered gates.
- [EthID](https://ethid.org/) for the Ethereum identity stack and profile APIs.
- [Grails](https://grails.app/) for ENS management and market workflows.

If you are building with Ethereum accounts, these are the defaults we recommend
and the ecosystem primitives this package is designed to compose with.

## Install

```bash
pip install siwe-django
```

For the optional DRF views:

```bash
pip install "siwe-django[drf]"
```

For OpenAPI schemas (auto-generated by drf-spectacular):

```bash
pip install "siwe-django[drf,openapi]"
```

For the setup wizard CLI:

```bash
pip install "siwe-django[cli]"
siwe-django init        # patch settings.py + urls.py
siwe-django doctor      # diagnose an existing install (CI-friendly --json)
siwe-django init --template  # also add the starter sign-in template
siwe-django scaffold-templates  # add the starter sign-in template
siwe-django scaffold-templates --overwrite  # refresh the starter template
siwe-django migrate-from-payton # rewrite payton/django-siwe-auth references
```

## Configure

```python
INSTALLED_APPS = [
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "siwe_django",
]

AUTHENTICATION_BACKENDS = [
    "siwe_django.backend.SiweBackend",
    "django.contrib.auth.backends.ModelBackend",
]

SIWE_DJANGO = {
    "DOMAIN": "example.com",
    "URI": "https://example.com/",
    "STATEMENT": "Sign in with Ethereum.",
    "ALLOWED_CHAIN_IDS": [1, 11155111],
    "ETHID_ENABLED": True,
    "RPC_URLS": {
        1: "https://mainnet.infura.io/v3/...",
        11155111: "https://sepolia.infura.io/v3/...",
    },
}
```

Add the vanilla Django routes:

```python
from django.urls import include, path

urlpatterns = [
    path("auth/siwe/", include("siwe_django.urls")),
]
```

Or the optional DRF routes:

```python
urlpatterns = [
    path("api/auth/siwe/", include("siwe_django.drf.urls")),
]
```

Run migrations:

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

The package URLconf is API-only. It does not mount a sign-in page for you.
Projects can use these endpoints from React, Django templates, htmx, or any
other frontend.

## Endpoints

- `GET /nonce/`: returns `{ nonce, expiresAt, domain, uri, statement,
  ethereumIdentityKit }` and binds the nonce to the current Django session.
- `POST /verify/`: accepts `{ message, signature }`, verifies the SIWE message
  with strict domain, URI, chain, nonce, and bound optional-field
  (`Resources`, `Request ID`, `Not Before`) checks, logs in the user, and returns
  user and wallet data.
- `GET /me/`: returns the current authenticated SIWE identity.
- `POST /logout/`: destroys the Django session.
- `POST /link/`: links another verified wallet to the current user.
- `GET /wallets/`: lists the current user's wallets.
- `DELETE /wallets/<id>/`: unlinks a wallet.
- `GET /profile/<address-or-ens>/`: proxies a display-ready Ethereum Identity
  Kit profile from the Eth Follow public API.
- `POST /reauth/`: re-verifies a SIWE signature for the currently authenticated
  user (step-up). Stamps the session so `@require_recent_siwe(seconds=N)` can
  gate sensitive actions.

## Frontend Flow

1. Fetch `GET /auth/siwe/nonce/`.
2. Create an EIP-4361 SIWE message with the returned nonce, domain, URI, and
   statement.
3. Ask the wallet to sign the prepared SIWE message.
4. Submit `{ message, signature }` to `POST /auth/siwe/verify/`.

The server consumes each nonce after the first successful verification, so replay
attempts fail.

### Server-rendered starter

For Django-template projects, `siwe_django.forms.SiweVerifyForm` and
`siwe_django.template_views.SiweLoginView` provide small UI primitives without
forcing a site layout. You can mount the starter view yourself:

```python
from django.urls import include, path
from siwe_django.template_views import SiweLoginView

urlpatterns = [
    path("auth/siwe/", include("siwe_django.urls")),
    path("login/siwe/", SiweLoginView.as_view(), name="siwe-login"),
]
```

The bundled `siwe_django/siwe_login.html` is intentionally unstyled. It keeps
only the stable DOM hooks used by the starter script and can be overridden in
your project templates or copied with:

```bash
siwe-django scaffold-templates
```

The starter is assembled from replaceable partials:

| Context key | Default template |
| --- | --- |
| `siwe_form_template_name` | `siwe_django/partials/form.html` |
| `siwe_button_template_name` | `siwe_django/partials/button.html` |
| `siwe_status_template_name` | `siwe_django/partials/status.html` |
| `siwe_result_template_name` | `siwe_django/partials/result.html` |
| `siwe_script_template_name` | `siwe_django/partials/script.html` |

Override those keys with `SiweLoginView.as_view(extra_context={...})` or by
subclassing `SiweLoginView` and replacing the matching `*_template_name`
attribute. The default script expects the rendered form/button/status/result
partials to keep the `siwe-form`, `siwe-submit`, `siwe-status`, and
`siwe-result` element IDs.

### Optional EIP-4361 fields

`siwe_django.services.issue_nonce` accepts `resources`, `request_id`, and
`not_before` keyword arguments and binds them to the issued nonce. When the
client signs a message that uses these fields, `verify_siwe_message` enforces
that:

- the signed `Resources` are a subset of the issued `resources`,
- the signed `Request ID` matches the bound value,
- the signed `Not Before` matches the bound timestamp.

### ReCap (ERC-5573)

`siwe_django.recap` ships helpers to build and parse ReCap capability URIs so
relying parties can scope a sign-in to specific abilities:

```python
from siwe_django.recap import encode_recap
from siwe_django.services import issue_nonce

recap_uri = encode_recap({"https://api.example.com": {"crud/read": [{}]}})
issue_nonce(request, resources=["https://api.example.com", recap_uri])
```

`siwe_django.recap.find_recap_in_resources(resources)` returns the decoded
`{"att": ..., "prf": ...}` payload from a SIWE message's `Resources` list, or
`None` when no ReCap is present.

### Smart contract wallets

`siwe-django` verifies smart contract wallet signatures via:

- **EIP-1271** for already-deployed contract wallets (Safe, multisigs, …).
- **EIP-6492** for counterfactual wallets that have not yet been deployed
  (Coinbase Smart Wallet, Privy, …). The upstream `signinwithethereum`
  library calls the EIP-6492 universal validator over `eth_call` so we never
  need a deployed contract.

Both paths require `RPC_URLS` to contain a provider for the wallet's chain.
Without it the contract check fails and the request is rejected.

## Showcase Demo

The repository includes a full Django + Vite React demo under
`examples/showcase/`. It uses the local package, Ethereum Identity Kit, Reown
AppKit, Wagmi, Viem, DRF, Django sessions, ENS/EthID profile enrichment, linked
wallets, and a custom local token gate that syncs the `demo-holders` Django
group.

```bash
cd examples/showcase/backend
uv run python manage.py migrate
uv run python manage.py runserver 127.0.0.1:8000

cd ../frontend
npm install
npm run dev
```

Open `http://localhost:5173`. See `examples/showcase/README.md` for optional
Reown, RPC, ENS, EthID, and demo gate environment variables.

## Ethereum Identity Kit

Ethereum Identity Kit is a React component library for SIWE, ENS profiles, and
EFP social data. `siwe-django` stays framework-agnostic on the backend while
returning the exact data frontend integrations need.

The nonce response includes `ethereumIdentityKit` metadata:

```json
{
  "nonce": "abc123...",
  "ethereumIdentityKit": {
    "statement": "Sign in with Ethereum.",
    "expirationTime": 300000,
    "messageParams": {
      "domain": "example.com",
      "uri": "https://example.com/",
      "version": "1",
      "nonce": "abc123..."
    }
  }
}
```

With `ethereum-identity-kit`:

```tsx
import { useSiwe } from "ethereum-identity-kit";

const { handleSignIn } = useSiwe({
  getNonce: async () => {
    const response = await fetch("/auth/siwe/nonce/", { credentials: "include" });
    const data = await response.json();
    return data.nonce;
  },
  verifySignature: async (message, _nonce, signature) => {
    const response = await fetch("/auth/siwe/verify/", {
      method: "POST",
      credentials: "include",
      headers: { "Content-Type": "application/json", "X-CSRFToken": csrfToken },
      body: JSON.stringify({ message, signature }),
    });
    return response.ok;
  },
  statement: "Sign in with Ethereum.",
  expirationTime: 300000,
});
```

When `ETHID_ENABLED` is true, login/linking stores display-ready profile data on
`SiweWallet`: ENS records, header, description, display name, avatar, EFP profile
URL, follower count, following count, and the raw EthID profile payload.
Serialized wallet responses include `displayName`, `avatar`, `profile`, and
`ethereumIdentityKit.addressOrName` for direct use with profile cards, avatars,
and tooltips.

## Existing Users and Wallet-Native Users

By default, `SiweWallet` links Ethereum wallets to `settings.AUTH_USER_MODEL`.
This is the best fit for existing Django applications.

Projects that want wallets to be the primary user identity can set:

```python
AUTH_USER_MODEL = "siwe_django.EthereumUser"
```

Set this before the first migration, as with any Django custom user model.

## Settings

All settings live under `SIWE_DJANGO`.

| Setting | Default | Purpose |
| --- | --- | --- |
| `DOMAIN` | request host | Expected SIWE domain. Set explicitly behind proxies. |
| `URI` | request root URI | Expected SIWE URI. |
| `STATEMENT` | `"Sign in with Ethereum."` | Human-readable statement for clients. |
| `NONCE_TTL_SECONDS` | `300` | Nonce lifetime. |
| `CLOCK_SKEW_SECONDS` | `60` | Tolerance applied to `Issued At`, `Not Before`, and `Expiration Time` checks. Set to `0` for strict comparison. |
| `ALLOWED_CHAIN_IDS` | `None` | Optional allow-list for message chain IDs. |
| `RPC_URLS` | `{}` | Chain ID to RPC URL map for contract wallet and token checks. |
| `ENS_ENABLED` | `False` | Enable ENS name/avatar lookup. |
| `ENS_RPC_URL` | `None` | RPC URL used for ENS lookup. |
| `ETHID_ENABLED` | `False` | Enrich wallets from Ethereum Identity Kit / Eth Follow APIs during auth. |
| `ETHID_PROFILE_PROXY_ENABLED` | `True` | Enable public `GET /profile/<address-or-ens>/` proxy endpoint. |
| `ETHID_API_BASE_URL` | `https://api.ethfollow.xyz/api/v1` | EthID/EFP API root. |
| `ETHID_TIMEOUT_SECONDS` | `2` | Timeout for EthID API calls. |
| `ETHID_CACHE_FRESH` | `False` | Request fresh EthID data instead of cached API data. |
| `AUTO_CREATE_USERS` | `True` | Create a user when a new wallet signs in. |
| `USER_FACTORY` | built-in | Dotted path for custom user creation. |
| `RATE_LIMITS` | `{}` | Optional per-view limits like `{ "verify": "5/m" }`. |
| `RATE_LIMIT_TRUST_X_FORWARDED_FOR` | `False` | Use the first `X-Forwarded-For` address for rate limits. Enable only behind a trusted proxy that strips client-supplied forwarding headers. |
| `AUDIT_ENABLED` | `True` | Persist sign-in events to `SiweAuthEvent`. Disable to forward audit data through your own pipeline. |
| `NONCE_STORE` | `siwe_django.nonce_store.DjangoOrmNonceStore` | Dotted path to the nonce store class. Swap for `siwe_django.nonce_store.RedisNonceStore` (extra: `pip install "siwe-django[redis]"`) for a Redis-backed store. |
| `REDIS_URL` | `None` | Used by `RedisNonceStore` when no client is injected. |
| `WEBHOOKS` | `[]` | Subscribers shaped `{event, url, secret, timeout?}`. `event: "*"` matches every audit event. Bodies are HMAC-SHA256 signed in the `X-Siwe-Signature` header. |
| `WEBHOOK_DISPATCHER` | `None` | Dotted path to a callable `(event, payload, subscriptions)` invoked instead of the synchronous urllib delivery (use to wire Celery / RQ). |
| `TOKEN_GATES` | `[]` | Optional group sync gates. |
| `SYNC_TOKEN_GATES_ON_LOGIN` | `True` | Sync token gates after login/linking. |

## Token Gates

Token gates sync Django `Group` membership and fail closed when an RPC URL is
missing or a check errors.

```python
SIWE_DJANGO = {
    "RPC_URLS": {1: "https://mainnet.infura.io/v3/..."},
    "TOKEN_GATES": [
        {
            "type": "erc721",
            "chain_id": 1,
            "contract": "0x...",
            "group": "nft-holders",
        },
        {
            "type": "custom",
            "checker": "myapp.siwe_gates.is_member",
            "group": "members",
        },
    ],
}
```

Custom checkers receive `wallet` and `gate` keyword arguments and return a
boolean.

### EFP and ENS gates

Gates are not limited to on-chain holdings. The Ethereum Identity Kit / EFP
graph is a first-class authorization primitive:

```python
SIWE_DJANGO = {
    "ETHID_ENABLED": True,
    "TOKEN_GATES": [
        {"type": "efp_followed_by", "source": "team.example.eth", "group": "team"},
        {"type": "efp_min_followers", "threshold": 100, "group": "popular"},
        {"type": "efp_tag", "source": "team.example.eth", "tag": "vip", "group": "vip"},
        {"type": "efp_not_blocked_by", "source": "team.example.eth", "group": "members"},
        {"type": "ens_required", "group": "ens-holders"},
    ],
}
```

| Type | Passes when |
| --- | --- |
| `efp_follower_of` | wallet follows `target` |
| `efp_followed_by` | `source` follows the wallet |
| `efp_mutual` | wallet and `hub` follow each other |
| `efp_min_followers` | wallet has at least `threshold` followers |
| `efp_tag` | `source` has tagged the wallet with `tag` |
| `efp_not_blocked_by` | `source` has not blocked or muted the wallet |
| `ens_required` | wallet has a primary ENS name |

EFP and ENS gates ignore `chain_id`. They reuse the existing `TOKEN_GATES`
group-sync semantics, so a failed gate removes the matching `Group` rather
than blocking sign-in.

## OIDC Helpers

`siwe_django.oidc.claims_for_wallet(wallet)` returns claim shapes compatible with
future SIWE OIDC integration:

```python
{
    "sub": "eip155:1:0x...",
    "preferred_username": "alice.eth",
    "picture": "https://...",
    "profile": "https://efp.app/alice.eth",
    "followers_count": 5368,
    "following_count": 10,
}
```

This package does not implement an OIDC provider in v1.

## Development

```bash
uv sync --extra drf --group dev
uv run ruff check
uv run pytest
uv run python -m build
```
