Custom Django authentication backend using Sign-In with Ethereum (EIP-4361) with a custom wallet user model.
Project description
Django Sign-In with Ethereum Auth
A complete Django authentication system overhaul for Web3. Treat Ethereum wallets as first-class citizens with authentication via Sign-In with Ethereum (EIP-4361) and authorization via on-chain attributes such as NFT (ERC721/ERC1155) and ERC20 token ownership.
DISCLAIMER: django-siwe-auth is still in early development (it will change often!)
Explore the docs »Live Demo · Report Bug · Request Feature
Table of Contents
About The Project
This app provides four key features for new or existing Django projects.
- Authentication Backend
- Authenticate incoming requests via the Sign-In with Ethereum (EIP-4361) standard.
- User Model
- Replace original user model with wallet model that is dependent on an Ethereum address.
- Removes the liability of storing user credentials in favor of managing transient session data (handled by Django out of the box).
- ENS profile information is pulled by default.
- Groups
- Create custom user groups based on on-chain (or off-chain!) attributes.
- For example, we can easily define a group of BAYC owners by validating NFT ownership and subsequently serve them special content that non-owners don't have access to.
- This extension builds off of Django's well-defined authorization system.
Below is the included example application that authenticates with an Ethereum address and utilizes on-chain attributes to authorize access to various notepads.
Getting Started
With the included example applications, you can test out Sign-In with Ethereum along with using and creating custom groups. To get an example application up and running follow these steps.
Prerequisites
Requirements for developing and running examples apps:
Demo Application
- Install NPM dependencies
npm --prefix examples/notepad/frontend install examples/notepad/frontend
- Build frontend
npm run --prefix examples/notepad/frontend build
- Install Python dependencies
poetry install
- Set Web3 provider environment variable
export SIWE_AUTH_PROVIDER="https://mainnet.infura.io/v3/..." # Any provider works
- Django migrations
# Create Django migrations poetry run examples/notepad/manage.py makemigrations # Apply Django migrations poetry run examples/notepad/manage.py migrate
- Run server
poetry run examples/notepad/manage.py runserver
- Test application at
https://localhost:8000
Rebuild Demo JavaScript
If you make an update to the frontend directory or siwe submodule, rebuild bundle.js:
cd examples/notepad/frontend \
npm install \
npm run build
Usage
This project is highly configurable and modular allowing for applications to be as simple or complex as required. Authentication and authorization can be completely replaced or supplemented depending upon the use case. See scenario docs for detailed tutorials.
Useful Resources
- User authentication in Django
- Customizing authentication in Django
- How to Switch to a Custom Django User Model Mid-Project
Install
Prerequisites
Requirements for using django-siwe-auth
in a Django application:
- pip
- django
- An existing Django project
- Install package
pip install django-siwe-auth
- Add
siwe_auth.apps.SiweAuthConfig
to INSTALLED_APPS in your project'ssettings.py
INSTALLED_APPS = [ ... "siwe_auth.apps.SiweAuthConfig", # Adds django-siwe-auth "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", ]
- Add app-specific definitions to your project's
settings.py
(same files as previous step). For more information, see the Configuring Your Project sectionfrom siwe_auth.custom_groups.erc721 import ERC721OwnerManager ... # Django Sign-In with Ethereum Auth Settings AUTH_USER_MODEL = "siwe_auth.Wallet" AUTHENTICATION_BACKENDS = ["siwe_auth.backend.SiweBackend"] LOGIN_URL = "/" SESSION_COOKIE_AGE = 3 * 60 * 60 CREATE_GROUPS_ON_AUTHN = False # defaults to False CREATE_ENS_PROFILE_ON_AUTHN = True # defaults to True CUSTOM_GROUPS = [ ('ens_owners', ERC721OwnerManager(config={'contract': '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85'})), ('bayc_owners', ERC721OwnerManager(config={'contract': '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D'})), ('shib_owners', ERC20OwnerManager(config={'contract': '0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce'})), ] # See "Group Plugins" section PROVIDER = os.environ.get("SIWE_AUTH_PROVIDER", "https://mainnet.infura.io/v3/...") ...
- Add
api/auth/
endpoint to your project'surls.py
urlpatterns = [ path("admin/", admin.site.urls), path("api/auth/", include("siwe_auth.urls")), ... ]
- Run
manage.py makemigrations
andmanage.py migrate
to create custom user model - Update frontend to interface with the REST API as defined below. An example frontend is available in the notepad demo.
- Never handle a password ever again!
For more examples, please refer to the Documentation
REST API
Projects that use this app will interact via these endpoints for authentication. Tools for authorization are provided but not enforced via any endpoints.
Login
- Authenticate user based on signed message.
/api/auth/login
-
Method:
POST
-
URL Params
None
-
Data Params
Required:
{"message": **SIWE message dict or formatted string**, "signature": **user signature of prior message**}
-
Success Response:
- Code: 200
Content:{"success": True, "message": "Successful login."}
- Code: 200
-
Error Response:
- Code: 401 UNAUTHORIZED
Content:{"success": False, "message": "Wallet disabled."}
OR
- Code: 403 FORBIDDEN
Content:{"success": False, "message": "Invalid login."}
OR
- Code: 429 TOO MANY REQUESTS
Content:{"message": "Too many requests. Slow down."}
- Code: 401 UNAUTHORIZED
-
Sample Call:
const res = await fetch(`/api/auth/login`, {
method: "POST",
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.getElementsByName('csrfmiddlewaretoken')[0].value,
},
body: JSON.stringify({ message, signature }),
credentials: 'include'
});
Logout
- Logout authenticated user.
/api/auth/logout
-
Method:
POST
-
URL Params
None
-
Data Params
None
-
Success Response:
- Code: 302
Redirect:/
- Code: 302
-
Error Response:
- Code: 429 TOO MANY REQUESTS
Content:{"message": "Too many requests. Slow down."}
- Code: 429 TOO MANY REQUESTS
-
Sample Call:
const res = await fetch(`/api/auth/logout`, {
method: "POST",
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.getElementsByName('csrfmiddlewaretoken')[0].value,
},
credentials: 'include'
});
Nonce
- Get temporary nonce to include in EIP-4361 (SIWE) message.
/api/auth/nonce
-
Method:
GET
-
URL Params
None
-
Data Params
None
-
Success Response:
- Code: 200
Content:{"nonce": **one-time use nonce**}
- Code: 200
-
Error Response:
- Code: 429 TOO MANY REQUESTS
Content:{"message": "Too many requests. Slow down."}
- Code: 429 TOO MANY REQUESTS
-
Sample Call:
const res = await fetch(`/api/auth/nonce`, {
credentials: 'include',
headers: {
'X-CSRFToken': document.getElementsByName('csrfmiddlewaretoken')[0].value,
},
});
Configuring Your Project
Relevant native Django settings
# in settings.py
AUTH_USER_MODEL = "siwe_auth.Wallet" # required for siwe as the default authentication
AUTHENTICATION_BACKENDS = ["siwe_auth.backend.SiweBackend"] # required for siwe as the default authentication
LOGIN_URL = "/" # optional, django's default is "/accounts/login/"
SESSION_COOKIE_AGE = 3 * 60 * 60 # Age of cookie, in seconds. Optional, django's default is weeks.
django-siwe-auth specific settings
# in settings.py
CREATE_GROUPS_ON_AUTHN = True # optional, default is False
CREATE_ENS_PROFILE_ON_AUTHN = True # optional, default is True
CUSTOM_GROUPS = [] # optional, see "Adding a Group" below
# Set environment variable SIWE_AUTH_PROVIDER for Web3.py
# - Required if CREATE_GROUPS_ON_AUTHN or CREATE_ENS_PROFILE_ON_AUTHN are True. Optional otherwise.
# Any ethereum API key (infura or alchemy) will work.
PROVIDER = os.environ.get("SIWE_AUTH_PROVIDER", "https://mainnet.infura.io/v3/...")
Override Admin Login Page
In order to login to the django admin page with siwe-auth, we need to override the login template.
# in your app's admin.py, add the following line
admin.site.login_template = 'siwe_auth/login.html'
Group Plugins
A group plugin allows you to define your own group whose membership is defined by the output of a membership function. Some examples of what the membership function may check for include but are not limited to: NFT ownership, ERC-20 token ownership, ENS data, etc.
GroupManager Class
All group plugins must implement the GroupManager class. This includes a function called is_member
that is called to determine group membership when a user authenticates with the server.
from abc import ABC, abstractmethod
class GroupManager(ABC):
@abstractmethod
def __init__(self, config: dict):
pass
@abstractmethod
def is_member(self, wallet: object) -> bool:
pass
Included Managers
- ERC20Manager
- ERC20OwnerManager
- ERC721Manager
- ERC721OwnerManager
- ERC1155Manager
- ERC1155OwnerManager
Adding a Group
A custom group is defined by a tuple consisting of a name (string), MemberManager implementation.
The GroupManager's config can be used for anything, but some likely usecases are defining contract addresses or address include/exclude lists.
In your project's settings.py
, append your group to the CUSTOM_GROUPS
list added in the installation steps.
Suppose we want to have only one group for BAYC
(ERC721 NFT) owners and another group for LPT
(ERC20 Token) owners. Our list would then look like:
from siwe_auth.custom_groups.erc721 import ERC721OwnerManager
from siwe_auth.custom_groups.erc20 import ERC20OwnerManager
...
CUSTOM_GROUPS=[
('bayc_owners', ERC721OwnerManager(config={'contract': '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d'})),
('lpt_owners', ERC20OwnerManager(config={'contract': '0x58b6a8a3302369daec383334672404ee733ab239'})),
]
Scenarios (WIP)
Examples:
- Start a new web app with wallet user model.
- Replace existing user model with wallet user model (non-trivial).
- Replace existing user model with weaker wallet user model that implements base model (less non-trivial but still non-trivial).
- Creating permissions around on-chain based groups.
Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
License
Distributed under the MIT License. See LICENSE.txt
for more information.
Contact
payton - @paytongarland - paytongarland.eth
Project Link: https://github.com/payton/django-siwe-auth
Acknowledgments
- Spruce - creators of EIP-4361
- jsdelivr - external static asset distribution in example app
- Best-README-Template
- 98.css
- API Documentation Format
Disclaimer
This django auth library has not yet undergone a formal security audit. We welcome continued feedback on the usability, architecture, and security of this implementation.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for django_siwe_auth-0.3.2-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 6b473ed5c452c59a85b3062185f4d6e62b4c1f0f6eb2c0bee30569dda26e01e0 |
|
MD5 | 6d35ce99ca1deeaa9f7efd8e1da32182 |
|
BLAKE2b-256 | 6e010c4d256763d2f549d7316eaea3f404e38d8778a9add4dbf30584cf6ddf45 |