Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- python-version: "3.10"
django: "5.2.*"
- python-version: "3.12"
django: "5.2.*"
- python-version: "3.12"
django: "6.0.*"
- python-version: "3.13"
django: "6.0.*"

steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Set up Python
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: |
uv sync --extra drf --group dev --python ${{ matrix.python-version }}
uv pip install "Django==${{ matrix.django }}"
- name: Ruff
run: uv run ruff check
- name: Tests
run: uv run pytest
- name: Build
run: uv run python -m build
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.venv/
.pytest_cache/
.ruff_cache/
dist/
build/
*.egg-info/
__pycache__/
*.py[cod]
.DS_Store
node_modules/
examples/showcase/backend/db.sqlite3
examples/showcase/frontend/dist/
*.tsbuildinfo
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 Nejc Drobnič
Copyright (c) 2026 EthID.org

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
267 changes: 266 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,266 @@
# siwe-better-auth
# siwe-django

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

`siwe-django` 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.

## Install

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

For the optional DRF views:

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

## 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
```

## 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, and nonce 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.

## 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.

## Showcase Demo

The repository includes a full Django + Vite React demo under
`examples/showcase/`. It uses the local package, Ethereum Identity Kit, 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
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, 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. |
| `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" }`. |
| `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.

## 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
```
61 changes: 61 additions & 0 deletions examples/showcase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# siwe-django Showcase

This demo runs a small Django backend and a Vite React frontend against the
local `siwe-django` package. It demonstrates nonce-based SIWE login, Django
sessions, linked wallets, ENS/EthID profile data, and token-gated Django groups.

## Backend

From the repository root:

```bash
uv sync --extra drf --group dev
cd examples/showcase/backend
uv run python manage.py migrate
uv run python manage.py runserver 127.0.0.1:8000
```

Optional environment:

```bash
export SIWE_DEMO_SECRET_KEY="dev-secret"
export SIWE_DEMO_ETHID_ENABLED="true"
export SIWE_DEMO_ENS_RPC_URL="https://mainnet.example/rpc"
export SIWE_DEMO_RPC_URL_1="https://mainnet.example/rpc"
export SIWE_DEMO_HOLDER_ADDRESSES="0xabc...,0xdef..."
```

The demo token gate is intentionally custom and local. It grants the
`demo-holders` Django group when the signed-in wallet address appears in
`SIWE_DEMO_HOLDER_ADDRESSES`.

## Frontend

In another terminal:

```bash
cd examples/showcase/frontend
npm install
npm run dev
```

Open `http://localhost:5173`. Vite proxies `/auth/siwe/` and
`/api/showcase/` to Django at `http://127.0.0.1:8000`, so no CORS package is
needed.

If port `8000` is already in use, run Django on another port and point Vite at
it:

```bash
uv run python manage.py runserver 127.0.0.1:8001
VITE_SHOWCASE_BACKEND_URL=http://127.0.0.1:8001 npm run dev
```

## Build Checks

```bash
uv run ruff check
uv run pytest
uv run python -m build
cd examples/showcase/frontend && npm run build
```
Loading
Loading