django-sns-signals 0.1
Django SNS handling and signals.
License: BSD-3-Clause
Requires Python: >=3.10

==================
django-sns-signals
==================

A Django library for handling and validating AWS SNS (Simple Notification Service) webhook messages with cryptographic signature verification and Django signals.

Features
========

* **Cryptographic Signature Verification** - Validates SNS message authenticity using AWS certificates
* **Django Signals** - Fire signals for subscription, unsubscription, and notification events
* **Auto-confirmation** - Automatically confirms SNS subscriptions and unsubscriptions
* **Certificate Caching** - Configurable caching of AWS signing certificates (default: 24 hours)
* **Minimal Dependencies** - Only requires Django and cryptography; uses Python's standard library for HTTP
* **Type Safety** - Full type hints with Python 3.10+

Installation
============

.. code-block:: bash

    pip install django-sns-signals

Requirements
------------

* Python 3.10+
* Django 4.2+
* cryptography ~=46.0.0

Quick Start
===========

1. Add to your Django URLs
---------------------------

.. code-block:: python

    # urls.py
    from django.urls import path
    from django_sns_signals.views import sns_webhook

    urlpatterns = [
        path('sns/webhook/', sns_webhook, name='sns_webhook'),
    ]

2. Connect to Django Signals
-----------------------------

.. code-block:: python

    # apps.py or signals.py
    from django.apps import AppConfig
    from django_sns_signals import sns_notification

    def handle_notification(sender, notification_message, topic_arn, subject=None, **kwargs):
        """Handle SNS notifications"""
        print(f"Received from {topic_arn}: {notification_message}")
        # Your business logic here

    class MyAppConfig(AppConfig):
        def ready(self):
            sns_notification.connect(handle_notification)

3. Configure SNS Topic (AWS Console)
-------------------------------------

1. Create an SNS topic in AWS
2. Add an HTTPS subscription with your webhook URL: ``https://yourdomain.com/sns/webhook/``
3. The library will automatically confirm the subscription

Configuration
=============

Optional Settings
-----------------

Add to your Django ``settings.py``:

.. code-block:: python

    # Optional: Configure signing certificate cache timeout (in seconds)
    # Default: 86400 (24 hours)
    SNS_SIGNING_CERT_CACHE_TIMEOUT = 86400

The certificate cache timeout controls how long AWS SNS signing certificates are cached. AWS rarely rotates these certificates, so a longer cache period (24 hours or more) is safe and reduces HTTP requests.

Architecture
============

Core Components
---------------

SNSSignatureValidator
~~~~~~~~~~~~~~~~~~~~~

Validates AWS SNS message signatures using cryptographic verification:

* Verifies certificate URLs come from ``amazonaws.com`` domain
* Implements certificate caching using Django's cache framework
* Builds canonical signing strings based on message type
* Uses RSA PKCS1v15 padding with SHA1 hash (AWS SNS standard)

sns_webhook View
~~~~~~~~~~~~~~~~

Django view function that handles SNS webhook POST requests:

* CSRF exempt (AWS cannot provide CSRF tokens)
* Validates message signatures before processing
* Handles three message types:

  - ``SubscriptionConfirmation``: Auto-confirms by fetching SubscribeURL
  - ``UnsubscribeConfirmation``: Auto-confirms unsubscription by fetching SubscribeURL
  - ``Notification``: Processes actual SNS notifications and fires signals

* Returns appropriate HTTP status codes (200-499 range required by SNS)

Django Signals
==============

The library fires Django signals for each SNS message type:

sns_subscription_confirmation
------------------------------

Fired after successful subscription confirmation.

**Signal Parameters:**

* ``message`` (dict) - Full parsed SNS message
* ``raw_message`` (bytes) - Raw request body
* ``message_id`` (str) - Unique message identifier
* ``topic_arn`` (str) - Topic ARN
* ``subject`` (str | None) - Optional subject
* ``timestamp`` (str) - Message timestamp
* ``subscribe_url`` (str) - Subscription confirmation URL
* ``token`` (str) - Subscription token

sns_unsubscribe_confirmation
-----------------------------

Fired after successful unsubscribe confirmation.

**Signal Parameters:**

* ``message`` (dict) - Full parsed SNS message
* ``raw_message`` (bytes) - Raw request body
* ``message_id`` (str) - Unique message identifier
* ``topic_arn`` (str) - Topic ARN
* ``subject`` (str | None) - Optional subject
* ``timestamp`` (str) - Message timestamp
* ``subscribe_url`` (str) - Unsubscribe confirmation URL
* ``token`` (str) - Unsubscription token

sns_notification
----------------

Fired when a notification is received.

**Signal Parameters:**

* ``message`` (dict) - Full parsed SNS message
* ``raw_message`` (bytes) - Raw request body
* ``message_id`` (str) - Unique message identifier
* ``topic_arn`` (str) - Topic ARN
* ``subject`` (str | None) - Optional subject
* ``timestamp`` (str) - Message timestamp
* ``notification_message`` (str) - The actual notification payload
* ``unsubscribe_url`` (str) - URL to unsubscribe from topic

Usage Examples
==============

Basic Notification Handler
---------------------------

.. code-block:: python

    from django_sns_signals import sns_notification

    def handle_sns_notification(sender, notification_message, topic_arn, subject=None, **kwargs):
        print(f"Topic: {topic_arn}")
        print(f"Subject: {subject}")
        print(f"Message: {notification_message}")

    sns_notification.connect(handle_sns_notification)

Topic-Specific Handler
----------------------

.. code-block:: python

    from django_sns_signals import sns_notification
    import json

    def handle_order_notifications(sender, notification_message, topic_arn, **kwargs):
        if 'orders' not in topic_arn:
            return  # Ignore non-order topics

        order_data = json.loads(notification_message)
        process_order(order_data)

    sns_notification.connect(handle_order_notifications)

Subscription Tracking
---------------------

.. code-block:: python

    from django_sns_signals import sns_subscription_confirmation, sns_unsubscribe_confirmation

    def log_subscription(sender, topic_arn, **kwargs):
        print(f"Subscribed to: {topic_arn}")

    def log_unsubscription(sender, topic_arn, **kwargs):
        print(f"Unsubscribed from: {topic_arn}")

    sns_subscription_confirmation.connect(log_subscription)
    sns_unsubscribe_confirmation.connect(log_unsubscription)

Security
========

Message Signature Validation
-----------------------------

All SNS messages are cryptographically validated before processing:

1. Extract ``SigningCertURL`` from SNS message
2. Validate URL hostname ends with ``.amazonaws.com``
3. Fetch certificate (cached for performance)
4. Build canonical string-to-sign based on message type
5. Verify signature using certificate's public key with PKCS1v15 padding and SHA1
6. Reject message if signature invalid

The library follows AWS best practices for SNS message verification as documented in the `AWS SNS Developer Guide <https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html>`_.

Development
===========

Setup
-----

.. code-block:: bash

    # Clone the repository
    git clone https://github.com/newadventures/django-sns-signals.git
    cd django-sns-signals

    # Install in development mode
    pip install -e .
    pip install -r requirements_dev.txt

Running Tests
-------------

.. code-block:: bash

    # Run tests with pytest
    pytest tests/

    # Or with Django's test runner
    python -m django test --settings=tests.settings

    # Run with coverage
    pytest --cov=django_sns_signals tests/

Code Quality
------------

.. code-block:: bash

    # Format and lint code
    ruff check src/ tests/
    ruff format src/ tests/

Building the Package
--------------------

.. code-block:: bash

    # Build distribution packages
    python -m build

    # Check package for PyPI compliance
    twine check dist/*

License
=======

BSD-3-Clause License - see LICENSE file for details.

Links
=====

* **GitHub**: https://github.com/newadventures/django-sns-signals
* **PyPI**: https://pypi.org/project/django-sns-signals/
* **AWS SNS Documentation**: https://docs.aws.amazon.com/sns/

Support
=======

For issues and questions, please use the `GitHub issue tracker <https://github.com/newadventures/django-sns-signals/issues>`_.