A reference implementation of enterprise SSO authentication with Django and AWS Cognito, supporting multiple OIDC providers (Azure AD, Okta). This repository accompanies a series of technical articles on building production-grade SSO flows.
The project demonstrates how to:
- Integrate AWS Cognito as an identity broker for external OIDC providers
- Support multiple SSO providers per tenant (Azure AD, Okta, generic OIDC)
- Map external identity provider roles to application-level roles
- Handle provider lifecycle events (user deprovisioning) via webhooks
- Structure a multi-app Django project with a clean layered architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Client / Browser │
└─────────────────┬───────────────────────────────┬───────────────────┘
│ │
▼ ▼
GET /api/oidc/check_config_exists GET /api/oidc/cognito/callback/
(discover SSO config by email) (exchange code for tokens)
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ Django (this repo) │
│ │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────────┐ │
│ │ SSOClient │ │ Cognito │ │ Notification │ │
│ │ Service │ │ Backend │ │ Service │ │
│ │ (sso.py) │ │ (backends.py)│ │ (notification.py) │ │
│ └──────────────┘ └───────────────┘ └──────────────────────┘ │
│ │ │ │ │
│ └──────────────────┴──────────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ PostgreSQL │ │
│ │ SSOAccount │ │
│ │ Config + │ │
│ │ Members │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
AWS Cognito User Pool Azure AD / Okta
(identity broker) (external OIDC providers)
apps/
├── accounts/ # Account (tenant) and membership management
│ ├── models.py # Account, Member
│ ├── permissions.py # DRF permission classes
│ ├── repositories/ # Data access layer
│ └── services/ # RightsHolderService
├── common/ # Shared base classes and utilities
│ ├── exceptions.py
│ ├── repositories/base.py
│ └── services/jwt.py
├── oidc_auth/ # Core SSO module
│ ├── backends.py # Django authentication backend (Cognito)
│ ├── models.py # SSOAccountConfig, NotificationProviderConfig
│ ├── tasks.py # Celery task: refresh Azure subscriptions
│ ├── api/
│ │ ├── authentication.py # OAuth2 callback handler
│ │ ├── cognito.py # Cognito setup/teardown endpoints
│ │ ├── notification.py # Azure / Okta webhook endpoints
│ │ └── views.py # SSO config lookup, logout
│ ├── repositories/
│ │ └── sso_account_config.py
│ └── services/
│ ├── sso.py # Core SSO orchestration
│ └── notification.py # Webhook event processing
└── users/ # Custom user model + Cognito helper
├── models.py
└── cognito.py # AWSCognitoHelper (boto3 wrapper)
1. Browser Django Cognito
| | |
|-- GET /check_config ----->| |
|<-- {client_id, ...} ------| |
| | |
|-- Redirect to Cognito ------------------------------->|
| | (user authenticates
| | with Azure/Okta/etc.)
|<-- Redirect with ?code=...&state=<client_id> ---------|
| | |
|-- GET /cognito/callback ->| |
| |-- POST /oauth2/token ---->|
| |<-- {id_token, ...} -------|
| | |
| | decode id_token
| | validate SSO config
| | map roles -> Member.role
| | create/update User + Member
| |
|<-- {access_token, ...} ---|
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/oidc/check_config_exists/<email>/ |
None | Look up SSO configs for an email domain |
| GET | /api/oidc/cognito/callback/ |
None | Handle OAuth2 authorization code callback |
| GET/POST | /api/oidc/cognito/logout/ |
None | Clear session |
| POST | /api/oidc/cognito/setup |
IsAuthenticated + RightsHolderOrManagementGroup |
Create Cognito identity provider + app client |
| DELETE | /api/oidc/cognito/delete |
IsAuthenticated + RightsHolder |
Remove Cognito SSO infrastructure |
| POST | /api/oidc/notifications/azure/ |
None (Azure-signed) | Receive Azure AD change notifications |
| POST | /api/oidc/notifications/okta/ |
OKTA_HOOK_AUTHORIZATION header |
Receive Okta event hooks |
Represents a tenant organisation. Can be of type RIGHTS_HOLDER, PARTNER, or MANAGEMENT_GROUP.
Join table linking User to Account with a role (ADMIN or MANAGER) and is_active flag used for soft-deletion during deprovisioning events.
One active SSO configuration per account. Stores the encrypted Cognito client_id / client_secret, allowed email domains, OIDC provider groups, and the issuer URL.
Azure-specific configuration attached to an SSOAccountConfig. Stores the credentials used to call the Microsoft Graph API and the webhook URL for change notifications.
- Docker + Docker Compose
- AWS account with a Cognito User Pool configured
git clone https://github.com/soldatov-ss/django-cognito-sso-demo.git
cd django-cognito-sso-demo
cp .env.sample .envEdit .env with your values:
# Database
POSTGRES_DB=django_db
POSTGRES_USER=django_user
POSTGRES_PASSWORD=django_password
DATABASE_URL=postgresql://django_user:django_password@db:5432/django_db
# Django
DEBUG=True
FIELD_ENCRYPTION_KEY=<generate: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())">
FRONT_END_ROOT=http://localhost:8000
# AWS Cognito (required for SSO)
COGNITO_AWS_REGION=us-east-1
COGNITO_USER_POOL=us-east-1_XXXXXXXXX
COGNITO_CLIENT_ID=your-app-client-id
COGNITO_DOMAIN=https://your-domain.auth.us-east-1.amazoncognito.com
USE_COGNITO=True
COGNITO_STRICT_SYNC=Truedocker compose up --buildThis runs migrations and starts the development server at http://localhost:8000.
docker compose run --rm web uv run pytestOr inside a running container:
uv run pytestThe demo assumes a Cognito User Pool configured with:
-
Custom attributes on the User Pool:
custom:groups— groups from the external OIDC providercustom:roles— roles from the external OIDC provider
-
An App Client with:
- Authorization code grant flow enabled
- Callback URL pointing to
/api/oidc/cognito/callback/ - Read/write access to the custom attributes above
-
External identity providers (Azure AD, Okta, etc.) are provisioned programmatically via
POST /api/oidc/cognito/setuponce a tenant's SSO configuration is created.
get_rights_holder_from_request is intentionally simplified for this demo. In a production system, the rights holder would typically be resolved from a partnership or tenant context injected by middleware. Here it resolves the caller's first active RIGHTS_HOLDER account directly from the request user — replace this with your own tenant resolution logic.
External provider roles (admin, manager) are mapped to application MemberRoles in SSOClientService._map_sso_roles_to_app_role. Extend this mapping to match your provider's group/role naming.
When Azure AD or Okta reports that a user has been removed from a synced group or deleted from the directory, Member.soft_delete() is called — it sets is_active=False without removing the database record. User.active_accounts() filters by this flag throughout the codebase.
SSOAccountConfig.encrypted_client_id and encrypted_client_secret use django-encrypted-model-fields. The FIELD_ENCRYPTION_KEY environment variable must be a valid Fernet key. A default development key is provided in settings.py — never use it in production.
uv run ruff check .
uv run ruff format .pip install pre-commit
pre-commit installThis package is licensed under the MIT License. See the LICENSE file for more details.