Skip to main content

Django authentication package using Electronic Digital Signatures (ECP/ЕЦП)

Project description

django-ecp-auth

Python 3.12+ Django 5.0+ License: MIT

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_success and ecp_user_registered events.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_ecp_auth-0.1.1.tar.gz (51.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_ecp_auth-0.1.1-py3-none-any.whl (49.3 kB view details)

Uploaded Python 3

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

Hashes for django_ecp_auth-0.1.1.tar.gz
Algorithm Hash digest
SHA256 4d3aa4b6b790727aa4fe0a3edecb3590b4cd0f4f7495ee9d5d2298f18afbf62e
MD5 ca5bb84929b70412c444af2faa57234d
BLAKE2b-256 dba36781e959e3bd7415320df80f06678c8e6db6b49caf3dffc7f888bfd0a666

See more details on using hashes here.

File details

Details for the file django_ecp_auth-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_ecp_auth-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 72dc33415d497f4d3b2e4b9beb7e56c679bb26fa3c07105c9cb15654a7e936d3
MD5 c4bb5152d3ca2e0da5e2ff6156c40fef
BLAKE2b-256 700544cfdf34ed41d7568b904a2c0a29ba98e1a1d011278656066dc255e10439

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page