A Django authentication middleware package for integrating with OUA SSO server
Project description
Organization Unified Access Authentication
A plug-and-play Django authentication package for integrating with the Organisation SSO backend. Supports JWKS-based JWT verification, tenant hierarchy (Country → Company → Branch), OAuth2 authorization code flow with PKCE, and hierarchical DRF permission classes.
Installation
pip install oua-auth
Quick Start
# settings.py
INSTALLED_APPS = [..., "oua_auth"]
OUA_SSO_URL = "https://sso.org.com"
OUA_CLIENT_ID = "your-client-id"
OUA_CLIENT_SECRET = "your-client-secret"
OUA_REDIRECT_URI = "https://yourapp.com/sso/callback/"
OUA_AUDIENCE = "organisation-services"
OUA_APPLICATION_CODE = "APPLICATION_CODE"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"oua_auth.authentication.OUAJWTAuthentication",
],
}
MIDDLEWARE = [
...,
"oua_auth.middleware.OUAAuthMiddleware",
"oua_auth.middleware.TenantScopeMiddleware",
"oua_auth.security_middleware.SecurityHeadersMiddleware",
]
# urls.py
urlpatterns = [
path("sso/", include("oua_auth.urls")),
...
]
Run migrations:
python manage.py migrate oua_auth
Configuration
Required Settings
| Setting | Description |
|---|---|
OUA_SSO_URL |
Organisation SSO server base URL |
OUA_CLIENT_ID |
OAuth client ID |
Auto-Discovered (OIDC)
These are fetched automatically from /.well-known/openid-configuration but can be overridden:
| Setting | Default |
|---|---|
OUA_ISSUER |
From OIDC discovery |
OUA_JWKS_URI |
{OUA_SSO_URL}/api/v1/oauth/keys |
OUA_AUDIENCE |
organisation-services |
OAuth Flow Settings
| Setting | Description |
|---|---|
OUA_CLIENT_SECRET |
OAuth client secret (for confidential clients) |
OUA_REDIRECT_URI |
Callback URL in your Django app |
OUA_SCOPES |
OAuth scopes (default: openid profile email) |
OUA_APPLICATION_CODE |
App code for role filtering (e.g. APPLICATION_CODE) |
OUA_LOGIN_REDIRECT_URL |
Where to redirect after login (default: /) |
Breaking changes in 0.8.0
- Identity and authorisation claims are no longer read from the JWT body.
Starting in 0.8.0,
oua_authresolves identity (email, preferred_username, name) fromGET /userinfoand role + tenant (sso_role, country_id, company_id, branch_ids) fromPOST /introspect. The JWT body carries onlysub,jti,typ,scp,azp,exp,iat,iss,aud,email_verified(v2 slim-token contract). Deployments must configure introspection credentials (OUA_INTROSPECTION_CLIENT_ID/OUA_INTROSPECTION_CLIENT_SECRET) or authentication fails closed with HTTP 503. - SSO unreachable → 503, not 401. When the SSO authority is unreachable and the
local cache is cold, the SDK returns 503 +
Retry-After— never a forced logout or 401. Stale cached claims are served within the grace window (OUA_REMOTE_CLAIMS_GRACE, default 300 s beyond TTL). - Seven new
OUA_*settings:OUA_INTROSPECT_ENDPOINT,OUA_INTROSPECTION_CLIENT_ID,OUA_INTROSPECTION_CLIENT_SECRET,OUA_REMOTE_CLAIMS_CACHE_TTL,OUA_REMOTE_CLAIMS_GRACE,OUA_REMOTE_CLAIMS_TIMEOUT,OUA_FETCH_USERINFO. See docs/configuration.md for defaults and details.
Breaking changes in 0.6.0
- Admin promotion fails closed (M-9). If both
OUA_TRUSTED_ADMIN_DOMAINSandOUA_TRUSTED_ADMIN_EMAILSare unset, no SSOSUPER_ADMINis auto-granted Djangois_staff/is_superuser(previous versions promoted everyone in that case). Set at least one to retain auto-promotion; otherwise provision admins manually. A startupWARNINGis logged when both are unset in production-mode config. - Session-restore verifies the token (C-1). Browser/session requests now re-verify the session access token via JWKS; an invalid token is cleared and the request proceeds unauthenticated.
- Static-PEM fallback is disabled by default (H-4). When
OUA_ALLOW_STATIC_KEY_FALLBACKis unset orFalse, a JWKS resolution failure results inAuthenticationFailed(401) instead of silently retrying againstOUA_PUBLIC_KEY. Clients that relied on the implicit fallback must setOUA_ALLOW_STATIC_KEY_FALLBACK = Trueexplicitly — but the recommended fix is to ensure JWKS reachability.
Security Settings
| Setting | Default | Description |
|---|---|---|
OUA_PUBLIC_KEY |
None |
Static PEM fallback key (PEM string). Only consulted when OUA_ALLOW_STATIC_KEY_FALLBACK is True. |
OUA_ALLOW_STATIC_KEY_FALLBACK |
False |
Security gate (H-4). When False (default), a JWKS resolution failure raises AuthenticationFailed immediately (fail-closed). Set to True only in environments where JWKS is genuinely unreachable and OUA_PUBLIC_KEY is a current, trusted key. A WARNING is logged every time the fallback fires. The correct long-term fix is ensuring JWKS reachability. |
OUA_TOKEN_LEEWAY |
0 |
Clock skew tolerance in seconds |
OUA_JWKS_REFETCH_COOLDOWN |
60 |
Minimum seconds between forced JWKS refetches on signature failure (H-3). A refetch is skipped — and InvalidSignatureError is raised immediately — if one already fired within this window. |
OUA_TRUSTED_ADMIN_DOMAINS |
[] |
Domains auto-granted Django admin (case-insensitive). Empty = no auto-promotion (fail-closed). |
OUA_TRUSTED_ADMIN_EMAILS |
[] |
Emails auto-granted Django admin. Empty = no auto-promotion (fail-closed). |
OUA_MAX_AUTH_FAILURES |
5 |
Failures before rate limiting |
OUA_AUTH_FAILURE_WINDOW |
300 |
Rate limit window in seconds |
OUA_ALLOWED_DOMAINS |
[] |
Only these email domains can authenticate |
OUA_RESTRICTED_DOMAINS |
[] |
These email domains are blocked |
OUA_MAX_SUSPICIOUS_ACTIVITIES |
3 |
Threshold for account locking |
SCIM consumer (oua_auth.scim)
Enable by adding oua_auth.scim.apps.OUAScimConfig to INSTALLED_APPS, mounting
oua_auth.scim.urls, and setting:
| Setting | Required | Default | Description |
|---|---|---|---|
OUA_SCIM_ADAPTER |
yes | — | Dotted path to your SCIMAdapter implementation |
OUA_SCIM_ROLE_EXTENSION_URN |
yes | — | SCIM extension URN carrying the per-app role list |
OUA_SCIM_PROVISION_SCOPE |
no | scim:provision |
OAuth scope a machine token must carry |
OUA_SCIM_IDENTITY_MODEL |
no | oua_scim.SCIMIdentity |
Swappable identity model (app_label.ModelName) |
OUA_PROVISION_CLIENT_ID / OUA_PROVISION_CLIENT_SECRET |
for dual-write | — | Machine client creds for outbound provisioning |
Migrating an existing consumer (db_table trap)
If your app already has a SCIM-identity table (e.g. you're extracting this
consumer out of an app that owned the table before), do not leave
OUA_SCIM_IDENTITY_MODEL at its default. The shipped default model binds a fresh
oua_scim_identity table, so a default install creates an empty table and
orphans your existing rows. To re-base onto your existing data:
- Subclass
AbstractSCIMIdentityin your own app. - Set the subclass's
Meta.db_tableto your existing identity table name. - Point
OUA_SCIM_IDENTITY_MODELat that subclass. LikeAUTH_USER_MODEL, the swap is resolved at app-registry load, so set this before startup (and before running migrations). - Generate the re-base migration and verify it is state-only — no
CREATE TABLE/ALTER TABLEagainst the live table (use aSeparateDatabaseAndState/--state-onlymigration). If the migration would rewrite the table, stop: the model and the existing schema are out of sync.
Authentication Flow
API Authentication (Bearer Token)
For API clients that already have a JWT access token from the SSO:
Authorization: Bearer <access-token>
The middleware validates the JWT via JWKS, provisions/updates the local Django user, and sets:
request.user— authenticated Django userrequest.oua_claims— decoded JWT payloadrequest.oua_tenant— dict withcountry_id,company_id,branch_id,sso_role,role_levelrequest.tenant_scope—TenantScopedataclass (set byTenantScopeMiddleware)
Browser Login (OAuth2 + PKCE)
For web apps that need to redirect users to the SSO login page:
- User visits a protected page
@sso_login_requiredredirects to/sso/login/- OUA generates PKCE challenge and redirects to SSO authorize endpoint
- User authenticates on SSO (with optional 2FA)
- SSO redirects back to
/sso/callback/with an authorization code - OUA exchanges the code for tokens, creates a Django session
- User is redirected to the original page
SSO Role Hierarchy
The SSO uses a hierarchical role model:
| Role | Level | Scope |
|---|---|---|
SUPER_ADMIN |
5 | Platform-wide |
COUNTRY_ADMIN |
4 | Country-wide |
COMPANY_ADMIN |
3 | Company-wide |
BRANCH_ADMIN |
2 | Branch-level |
USER |
1 | Basic access |
DRF Permission Classes
from oua_auth.permissions import HasSSORole, HasTenantAccess, IsSSOAuthenticated
class OrderViewSet(viewsets.ModelViewSet):
# Require at least COMPANY_ADMIN role
permission_classes = [HasSSORole("COMPANY_ADMIN")]
def get_queryset(self):
# Auto-filter by tenant scope
return Order.objects.for_tenant(self.request.tenant_scope)
class BranchDetailView(APIView):
# Check tenant access on object level
permission_classes = [IsSSOAuthenticated, HasTenantAccess]
View Decorators
from oua_auth.decorators import sso_login_required, require_sso_role
@sso_login_required
def dashboard(request):
return render(request, "dashboard.html")
@require_sso_role("BRANCH_ADMIN")
def admin_panel(request):
return render(request, "admin.html")
Tenant-Scoped Models
from oua_auth.models import TenantMixin
class Order(TenantMixin):
description = models.TextField()
amount = models.DecimalField(max_digits=10, decimal_places=2)
# In a view — automatic tenant filtering:
orders = Order.objects.for_tenant(request.tenant_scope)
# Manual tenant assignment on create:
Order.objects.create(
description="New order",
amount=100.00,
country_id=request.tenant_scope.country_id,
company_id=request.tenant_scope.company_id,
branch_id=request.tenant_scope.branch_id,
)
Token Blacklisting
from oua_auth.authentication import OUAJWTAuthentication
# Revoke a token
OUAJWTAuthentication.revoke_token(
token=request.oua_token,
blacklisted_by=request.user.email,
reason="User logout",
)
Clean up expired tokens:
python manage.py clean_expired_tokens
Security Features
- JWKS-based JWT verification with automatic key rotation retry
- Token type validation — rejects refresh tokens used as access tokens
- Tenant hierarchy — Country → Company → Branch scoping
- Hierarchical role model —
SUPER_ADMIN>COUNTRY_ADMIN>COMPANY_ADMIN>BRANCH_ADMIN>USER - Rate limiting — distributed via Django cache framework
- Token blacklisting — persistent DB + in-memory fallback
- Account locking — after configurable suspicious activity threshold
- Input sanitization — XSS prevention via lxml
- Security headers middleware — CSP, HSTS, X-Frame-Options, etc.
- Structured logging — JSON output with sensitive data redaction
Testing
pip install -e ".[test]"
python -m pytest
python -m pytest --cov=oua_auth --cov-report=term-missing
License
MIT License — see the LICENSE file 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 oua_auth-1.5.0.tar.gz.
File metadata
- Download URL: oua_auth-1.5.0.tar.gz
- Upload date:
- Size: 169.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d04e3746f7c760d07fe299acfd503a16beb6ebb05504654dd91b96227d1dd62
|
|
| MD5 |
7da525f7f80f49b883927e9cb7fdd821
|
|
| BLAKE2b-256 |
a5becb32c02945ce630d62a40fa48afa64a4842c43e59447f055312991b0701e
|
File details
Details for the file oua_auth-1.5.0-py3-none-any.whl.
File metadata
- Download URL: oua_auth-1.5.0-py3-none-any.whl
- Upload date:
- Size: 75.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bac885b1234f66bc2ea47480fcad86ee0749a6922403e0828691233336e05c15
|
|
| MD5 |
892c7486b3bac17864e71054b41b4406
|
|
| BLAKE2b-256 |
83a032cd1e27dbfc9f3a36d89e113310a251c076f60912122389a9def961fbaa
|