Django authentication package using Electronic Digital Signatures (ECP/ЕЦП)
Project description
django-ecp-auth
Seamless Electronic Digital Signature (ECP/ЕЦП) Authentication for Django.
django-ecp-auth is a reusable Django application providing robust user authentication and registration via X.509 digital certificates. It features a fully integrated "single-page" authentication flow, allowing users to log in with standard credentials or PKCS#12 (.p12/.pfx) files simultaneously.
Features
- 🔐 PKCS#12 Authentication — Log in effortlessly using ECP files (without separating normal and ECP login pages).
- 🛠️ Mixin-based Integration — Zero-friction integration into existing codebases via Django View and Form Mixins.
- 📝 Dynamic Certificate Generation — Let users generate a personalized ECP key during registration.
- 🛡️ Cryptographic Verification — Embedded nonce-based challenge/response signatures using
cryptography. - 🧩 Django Auth Backend — Fully compatible with standard Django authentication flows.
- 📡 Signals Engine — Hook into
ecp_login_successandecp_user_registeredevents.
Installation
Install via pip:
pip install django-ecp-auth
Інструкція зі швидкого старту (Full Copy-Paste) 🚀
Найпростіший спосіб інтегрувати бібліотеку — використати файли з нашого тестового робочого проекту. Нижче наведені повністю готові файли (скопіюйте та вставте у свій проект те, що вам потрібно).
1. 📁 Вставляти у: /settings.py (Налаштування)
"""
Django settings for backend project.
Generated by 'django-admin startproject' using Django 6.0.3.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
import os
from pathlib import Path
from urllib.parse import unquote, urlparse
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY',
'django-insecure-4cd8phcl_g)j$0wy!@zl5!sv*f$3ddv*7@jke(#p!uhw88%$s&',
)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DEBUG', 'False').lower() in {'1', 'true', 'yes', 'on'}
ALLOWED_HOSTS = [host.strip() for host in os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') if host.strip()]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'users',
'ecp_auth',
]
AUTH_USER_MODEL = 'users.User'
AUTHENTICATION_BACKENDS = [
'ecp_auth.backends.ECPAuthenticationBackend',
'django.contrib.auth.backends.ModelBackend',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'ecp_auth.middleware.ECPSessionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.jinja2.Jinja2',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'environment': 'backend.jinja2.environment',
'context_processors': [
'django.template.context_processors.request',
'django.template.context_processors.csrf',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.template.context_processors.csrf',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'backend.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASE_URL = os.environ.get('DATABASE_URL', '')
if DATABASE_URL:
parsed_db = urlparse(DATABASE_URL)
db_engines = {
'postgres': 'django.db.backends.postgresql',
'postgresql': 'django.db.backends.postgresql',
'sqlite': 'django.db.backends.sqlite3',
}
db_engine = db_engines.get(parsed_db.scheme)
if not db_engine:
raise ValueError(f'Unsupported DATABASE_URL scheme: {parsed_db.scheme}')
if db_engine == 'django.db.backends.sqlite3':
db_name = parsed_db.path.lstrip('/') or str(BASE_DIR / 'db.sqlite3')
DATABASES = {
'default': {
'ENGINE': db_engine,
'NAME': db_name,
}
}
else:
DATABASES = {
'default': {
'ENGINE': db_engine,
'NAME': parsed_db.path.lstrip('/'),
'USER': unquote(parsed_db.username or ''),
'PASSWORD': unquote(parsed_db.password or ''),
'HOST': parsed_db.hostname or '',
'PORT': str(parsed_db.port or ''),
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'dashboard'
LOGOUT_REDIRECT_URL = 'login'
2. 📁 Вставляти у: ваша_головна_папка/urls.py (Головний файл роутингу проекту)
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
# Ваші існуючі шляхи...
path("", include("users.urls")),
# Обов'язковий шлях до нашої бібліотеки
path('auth/', include('ecp_auth.urls')),
path('admin/', admin.site.urls),
]
3. 📁 Вставляти у: додаток_користувачів/views.py (Вікна входу та реєстрації проекту)
from django.contrib.auth.views import LoginView as DjangoLoginView
from django.urls import reverse_lazy
from django.views.generic import CreateView
from ecp_auth.mixins import ECPLoginViewMixin, ECPRegistrationViewMixin
from ecp_auth.forms import ECPLoginForm, ECPRegistrationFormMixin
from .forms import RegisterForm
# 1. Створюємо форму реєстрації, підмішавши ECPRegistrationFormMixin
class MyRegisterForm(ECPRegistrationFormMixin, RegisterForm):
pass
# 2. Представлення (View) реєстрації
class RegisterView(ECPRegistrationViewMixin, CreateView):
form_class = MyRegisterForm
template_name = "auth/register.html"
success_url = reverse_lazy("login")
# 3. Представлення (View) входу
class LoginView(ECPLoginViewMixin, DjangoLoginView):
# ECPLoginForm містить всі необхідні поля та логіку перевірки
form_class = ECPLoginForm
template_name = "auth/login.html"
redirect_authenticated_user = True
4. 📁 Вставляти у: templates/auth/login.html (Шаблон сторінки входу)
<!DOCTYPE html>
<html lang="uk">
<body class="bg-slate-100">
<main class="mx-auto flex min-h-screen items-center justify-center p-6">
<section class="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
<h1 class="text-2xl font-semibold">Вхід у систему</h1>
<!-- Сповіщення про помилки (якщо є) -->
{% if form.non_field_errors %}
<div class="mt-4 rounded-lg bg-rose-50 text-rose-700 p-3">
{% for error in form.non_field_errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
<!-- Обов'язково додайте enctype="multipart/form-data" -->
<form action="{% url 'login' %}" method="post" enctype="multipart/form-data" class="mt-6 space-y-4">
{% csrf_token %}
<!-- Блок звичайного логіна/пароля -->
<div>
<label for="id_username">Логін</label>
<input id="id_username" name="username" type="text" class="w-full border rounded p-2">
</div>
<div>
<label for="id_password">Пароль</label>
<input id="id_password" name="password" type="password" class="w-full border rounded p-2">
</div>
<hr class="my-6">
<p class="text-sm font-semibold text-slate-500 mb-2">АБО УВІЙДІТЬ ЗА ЕЦП КЛЮЧЕМ</p>
<!-- Блок ЕЦП-авторизації -->
<div>
<label for="id_pkcs12_file">Файл з ключем (.p12 / .pfx)</label>
{{ form.pkcs12_file }}
{% if form.pkcs12_file.errors %}
<div class="text-xs text-rose-600">{{ form.pkcs12_file.errors.0 }}</div>
{% endif %}
</div>
<div>
<label for="id_pkcs12_password">Пароль ключа</label>
{{ form.pkcs12_password }}
{% if form.pkcs12_password.errors %}
<div class="text-xs text-rose-600">{{ form.pkcs12_password.errors.0 }}</div>
{% endif %}
</div>
<button type="submit" class="mt-4 w-full bg-slate-900 text-white p-2 rounded">
Увійти
</button>
</form>
</section>
</main>
</body>
</html>
5. 📁 Вставляти у: templates/auth/register.html (Шаблон сторінки реєстрації)
<!DOCTYPE html>
<html lang="uk">
<body class="bg-slate-100">
<main class="mx-auto flex min-h-screen items-center justify-center p-6">
<section class="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
<h1 class="text-2xl font-semibold">Реєстрація</h1>
<!-- Обов'язково додайте enctype="multipart/form-data" -->
<form action="{% url 'register' %}" method="post" enctype="multipart/form-data" class="mt-6 space-y-4">
{% csrf_token %}
<!-- Ваші стандартні поля реєстрації (username, password тощо) -->
<div>
<label for="id_reg_username">Логін</label>
<input id="id_reg_username" name="username" type="text" value="{{ form.username.value|default_if_none:'' }}" class="w-full border rounded p-2">
</div>
<div>
<label for="id_reg_password1">Пароль</label>
<input id="id_reg_password1" name="password1" type="password" class="w-full border rounded p-2">
</div>
<div>
<label for="id_reg_password2">Підтвердження пароля</label>
<input id="id_reg_password2" name="password2" type="password" class="w-full border rounded p-2">
</div>
<hr class="my-6">
<p class="text-sm font-semibold text-slate-500 mb-2">ОПЦІОНАЛЬНО: ЕЦП (ДЛЯ ШВИДКОГО ВХОДУ)</p>
<!-- Блок генерації ЕЦП -->
<div class="flex items-center">
{{ form.generate_ecp }}
<label for="id_generate_ecp" class="ml-2 text-sm text-slate-700">Згенерувати ЕЦП ключ</label>
</div>
<p class="text-xs text-slate-500 mb-4">{{ form.generate_ecp.help_text }}</p>
<div>
<label for="id_ecp_password">Пароль до ключа</label>
{{ form.ecp_password }}
{% if form.ecp_password.errors %}
<div class="text-xs text-rose-600">{{ form.ecp_password.errors.0 }}</div>
{% endif %}
</div>
<button type="submit" class="mt-4 w-full bg-slate-900 text-white p-2 rounded">
Зареєструватися
</button>
</form>
</section>
</main>
</body>
</html>
Після додавання цих файлів:
python manage.py migrate
python manage.py runserver
Signal Hooks
Extend functionality using our built-in signals:
from django.dispatch import receiver
from ecp_auth.signals import ecp_login_success, ecp_user_registered
@receiver(ecp_login_success)
def handle_ecp_login(sender, request, user, certificate_info, **kwargs):
print(f"Welcome {user.username}! Assured by cert: {certificate_info.fingerprint_sha256}")
Development & Testing
git clone https://github.com/ecp-auth/django-ecp-auth.git
cd django-ecp-auth
pip install -e ".[dev]"
pytest tests/ -v
License
MIT License. See LICENSE for details.
Configuration
Add these settings to your settings.py (all are optional):
# Challenge-Response
ECP_AUTH_CHALLENGE_TIMEOUT = 300 # Nonce timeout in seconds (default: 5 min)
ECP_AUTH_CHALLENGE_LENGTH = 32 # Nonce length in bytes (default: 32)
# Certificate Validation
ECP_AUTH_REQUIRE_KEY_USAGE = True # Require digitalSignature key usage
ECP_AUTH_ALLOW_SELF_SIGNED = True # Allow self-signed certificates
ECP_AUTH_VALIDATE_CHAIN = False # Enable certificate chain validation
ECP_AUTH_TRUSTED_CA_DIR = '/path/to/ca/certs' # Trusted CA directory
# Redirects
ECP_AUTH_LOGIN_REDIRECT_URL = '/' # After login
ECP_AUTH_LOGOUT_REDIRECT_URL = '/auth/login/' # After logout
Authentication Flow
1. User uploads PKCS#12 file (.p12/.pfx) with password
2. Server extracts X.509 certificate and private key
3. Server validates the certificate (expiration, key usage, etc.)
4. Server generates a random challenge nonce
5. Server signs the nonce with the user's private key
6. User confirms the signature
7. Server verifies the signature against the certificate
8. User is authenticated and logged in
Signals
Connect to authentication events:
from ecp_auth.signals import ecp_login_success, ecp_user_registered
@receiver(ecp_login_success)
def on_ecp_login(sender, request, user, certificate_info, **kwargs):
print(f"User {user.username} logged in via ECP")
@receiver(ecp_user_registered)
def on_ecp_register(sender, request, user, certificate_info, **kwargs):
print(f"New user registered: {user.username}")
Available Signals
| Signal | Arguments |
|---|---|
ecp_login_success |
request, user, certificate_info |
ecp_login_failed |
request, reason, certificate_info |
ecp_user_registered |
request, user, certificate_info |
ecp_logout |
request, user |
ecp_certificate_linked |
user, certificate_info |
Development
# Clone and install with dev dependencies
git clone https://github.com/ecp-auth/django-ecp-auth.git
cd django-ecp-auth
pip install -e ".[dev]"
# Run tests
pytest tests/ -v
# Run with coverage
pytest tests/ -v --cov=ecp_auth --cov-report=term-missing
Dependencies
- Django ≥ 5.0
- cryptography ≥ 42.0
- pycryptodome ≥ 3.20
- asn1crypto ≥ 1.5
- certvalidator ≥ 0.11
- Jinja2 ≥ 3.1
License
MIT License. See LICENSE for details.
Project details
Release history Release notifications | RSS feed
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
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_ecp_auth-0.1.1.tar.gz.
File metadata
- Download URL: django_ecp_auth-0.1.1.tar.gz
- Upload date:
- Size: 51.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d3aa4b6b790727aa4fe0a3edecb3590b4cd0f4f7495ee9d5d2298f18afbf62e
|
|
| MD5 |
ca5bb84929b70412c444af2faa57234d
|
|
| BLAKE2b-256 |
dba36781e959e3bd7415320df80f06678c8e6db6b49caf3dffc7f888bfd0a666
|
File details
Details for the file django_ecp_auth-0.1.1-py3-none-any.whl.
File metadata
- Download URL: django_ecp_auth-0.1.1-py3-none-any.whl
- Upload date:
- Size: 49.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72dc33415d497f4d3b2e4b9beb7e56c679bb26fa3c07105c9cb15654a7e936d3
|
|
| MD5 |
c4bb5152d3ca2e0da5e2ff6156c40fef
|
|
| BLAKE2b-256 |
700544cfdf34ed41d7568b904a2c0a29ba98e1a1d011278656066dc255e10439
|