Skip to content

v0.2: setup wizard, EIP-4361 strict mode, EFP/ENS gates, audit log + webhooks#3

Merged
Quantumlyy merged 16 commits into
mainfrom
siwe-django-wizard
Apr 28, 2026
Merged

v0.2: setup wizard, EIP-4361 strict mode, EFP/ENS gates, audit log + webhooks#3
Quantumlyy merged 16 commits into
mainfrom
siwe-django-wizard

Conversation

@Quantumlyy
Copy link
Copy Markdown
Collaborator

Adds a Typer-based siwe-django setup wizard (init / doctor / scaffold-templates / migrate-from-payton) under the new cli extra, a bundled Django sign-in template, and an examples/templates_demo/ non-React reference.

Tightens spec compliance: verify_siwe_message now binds Resources / Request ID / Not Before to the issued nonce, applies a CLOCK_SKEW_SECONDS tolerance, and the EIP-6492 counterfactual signature path is covered with tests; siwe_django.recap ships ERC-5573 helpers.

Turns the EthID/EFP social graph into an authorization primitive via seven new gate types (efp_follower_of, efp_followed_by, efp_mutual, efp_min_followers, efp_tag, efp_not_blocked_by, ens_required) plus typed helpers in ethid.py.

Adds production hardening: SiweAuthEvent audit log, pluggable NonceStore with a Redis backend (siwe-django[redis]), HMAC-SHA256 signed webhooks dispatched from audit events, step-up auth (POST /reauth/ + @require_recent_siwe), and opt-in drf-spectacular schemas (siwe-django[openapi]).

Bumps to v0.2.0; 151 tests pass.

`verify_siwe_message` now binds the optional EIP-4361 fields to the
issued nonce: signed `Resources` must be a subset of the issued list,
`Request ID` must match, and `Not Before` must match. A new
`CLOCK_SKEW_SECONDS` setting (default 60) tolerates small client/server
clock drift on `Issued At` / `Not Before` / `Expiration Time` checks via
the `timestamp` parameter on the upstream `siwe` library. The nonce
response surfaces the bound optional fields so clients can replay them
into the signed message verbatim.
Adds integration tests for the EIP-6492 path through `verify_siwe_message`,
asserting that wrapped signatures route through the upstream universal
validator and that the request is rejected when no RPC is available for
the chain. Documents the EIP-1271 / EIP-6492 contract wallet support in
the README.
`siwe_django.recap` ships `encode_recap`, `decode_recap`,
`find_recap_in_resources`, and `build_recap_statement` so apps can scope
sign-ins to specific capabilities. The encoded URN goes into the SIWE
message's `Resources` list as the final entry per spec; verification of
that ReCap is already covered by the new resources-subset check from the
EIP-4361 strict-mode commit.
Adds typed wrappers for the EthID/EFP API endpoints we need for the
upcoming EFP-gated sign-in flow:

- fetch_efp_stats(address) -> follower / following counts
- fetch_efp_follower_state(viewer, target) -> {follow, block, mute}
- fetch_efp_followers / fetch_efp_following with limit + offset
- fetch_efp_tags(address, source=...) with optional tagger filter
- fetch_ens_record(address)

All helpers degrade gracefully (return zeros / empty list) on lookup
failure so callers can compose them without nested error handling.
Extends the existing TOKEN_GATES dispatcher with seven new types backed
by the EthID/EFP API:

- efp_follower_of, efp_followed_by, efp_mutual: gate by social-graph
  edges between the wallet and a hub account.
- efp_min_followers: gate by absolute follower count.
- efp_tag: gate by tags applied to the wallet by a hub account.
- efp_not_blocked_by: gate by absence of block / mute records.
- ens_required: gate by presence of a primary ENS name.

EFP / ENS gates ignore chain_id and reuse the existing group-sync
semantics: a passing gate adds the configured Django group, a failing
one removes it. README updated with the gate type table.
Ships a Typer-based CLI behind the new `cli` extra so the runtime package
keeps zero CLI dependencies. Subcommands:

- `siwe-django init` — patches `settings.py` (INSTALLED_APPS,
  AUTHENTICATION_BACKENDS, SIWE_DJANGO block) and root `urls.py`
  (mounts siwe_django.urls or siwe_django.drf.urls) via libcst, then
  optionally drops a bundled Django sign-in template and runs
  `manage.py migrate`. Idempotent: re-running detects the no-op and
  exits cleanly.
- `siwe-django doctor` — diagnoses a configured project: pings RPC URLs,
  checks the EthID API, flags ALLOWED_CHAIN_IDS without RPCs, missing
  DOMAIN/URI. Supports `--json` for CI; non-zero exit on errors.
- `siwe-django scaffold-templates` — copies the bundled
  `templates/siwe_django/siwe_login.html` (a no-build, vanilla-JS sign-in
  page using `window.ethereum`) into the target project and registers a
  URL include.
- `siwe-django migrate-from-payton` — best-effort regex rewrite of a
  `payton/django-siwe-auth` project: renames the package, points imports
  at the new model class names, and prints a follow-up checklist for
  the parts that need a real data migration.

The bundled template also ships at
`src/siwe_django/templates/siwe_django/siwe_login.html` so adopters who
do not run the wizard can still `{% include %}` it.
A minimal Django project (no React, no build step) that renders the
bundled `siwe_django/siwe_login.html` template against the standard
SIWE endpoints. Mirrors the output of `siwe-django scaffold-templates`
and serves as the canonical no-JS-toolchain reference alongside the
existing React showcase.
Adds a `SiweAuthEvent` model that records nonce / verify / link / unlink /
logout events with IP, user-agent, success flag, error code, and a free-form
metadata JSON. Both the vanilla and DRF view layers call into a small
`audit.record_event` helper after each action. Logging is opt-in via the new
`AUDIT_ENABLED` setting (default True) so apps that route audit data
elsewhere can disable the DB writes without losing functionality.
Introduces a `NonceStore` protocol with a `NonceRecord` dataclass and
two implementations:

- `DjangoOrmNonceStore` (default) — wraps the existing SiweNonce model.
- `RedisNonceStore` (`siwe-django[redis]` extra) — uses `SET NX EX` for
  save and atomic delete for consume; replay protection is enforced
  because the second consumer's delete returns 0.

The store is resolved via the new `NONCE_STORE` dotted-path setting,
defaulting to the ORM backend so existing installs are unchanged.
`services.issue_nonce` / `_load_nonce` / `_consume_nonce` route through
the store instead of touching the model directly.
`SIWE_DJANGO["WEBHOOKS"]` accepts subscribers shaped
`{event, url, secret, timeout?}`. After every recorded auth event the
audit helper builds a canonical JSON payload, signs it with HMAC-SHA256
(`X-Siwe-Signature: sha256=<hex>`), and POSTs to each matching
subscriber. `event: "*"` is a wildcard.

Default delivery is synchronous best-effort urllib (failures are logged
but never block sign-in). Apps can plug in Celery / RQ by setting
`WEBHOOK_DISPATCHER` to a callable that receives
`(event, payload, subscriptions)`.
Adds a session-bound "last verified at" stamp written by every
successful verify (vanilla + DRF). New `POST /reauth/` accepts a fresh
SIWE message + signature for the authenticated user, refreshes the
stamp, and rejects messages signed by wallets not linked to the user.

Sensitive views can gate themselves with
`@require_recent_siwe(seconds=300)` which returns
`403 stepup_required` when the session has no fresh verify.
Adds an `openapi` extra (drf-spectacular>=0.27) and decorates every DRF
view with `@extend_schema(tags=["siwe"])` plus a short summary so apps
that wire drf-spectacular get a usable OpenAPI 3.1 contract for free.
A passthrough shim in `siwe_django.drf.schema` keeps the views importable
when drf-spectacular is not installed, so the `openapi` extra stays
strictly opt-in.
Captures the EIP-4361 strict-mode work, EIP-6492 coverage, ReCap
helpers, EFP / ENS social graph gates, the setup wizard CLI, the
bundled Django sign-in template, the audit log, the pluggable nonce
store with Redis backend, signed webhooks, step-up auth, and the
opt-in drf-spectacular schemas.
`json.dumps` was outside the try block, so a `TypeError` from richer
metadata (datetime, Decimal, model instance) propagated up through
dispatch_webhook → record_event → the verify view and surfaced as a
500 even though `auth_login` had already committed the session.
Now we serialize inside try/except and return False on failure,
matching the module's documented best-effort semantics.
Hard-coded `python` fails on systems that only ship `python3` (notably
stock macOS without a venv on PATH). subprocess.run then raises
FileNotFoundError instead of returning a non-zero exit code, so the
graceful failure branch is bypassed and the CLI crashes. Use
sys.executable so the migrate step always runs under the same
interpreter that's running the wizard.
Multi-stage Dockerfile builds the Vite bundle with bun, then serves it
plus the Django endpoints from a single gunicorn process behind
WhiteNoise so CSRF / sessions stay same-origin.

Settings are now env-driven (ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS,
DOMAIN, URI, database path, secure-cookie / X-Forwarded-Proto flags)
with the existing localhost defaults preserved for local dev. A new
catch-all SPA view returns the built index.html for any unmatched
path; Vite is taught to honour VITE_BASE so the production build emits
asset URLs under /static/.

`fly.toml` runs on shared-cpu-1x:512mb with a 1 GB volume mounted at
/data for SQLite and an explicit migrate release_command. The README
now documents the launch / volumes / secrets / deploy workflow.
@Quantumlyy Quantumlyy merged commit 760ba9d into main Apr 28, 2026
4 checks passed
@Quantumlyy Quantumlyy deleted the siwe-django-wizard branch April 28, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant