Skip to main content

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_auth resolves identity (email, preferred_username, name) from GET /userinfo and role + tenant (sso_role, country_id, company_id, branch_ids) from POST /introspect. The JWT body carries only sub, 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_DOMAINS and OUA_TRUSTED_ADMIN_EMAILS are unset, no SSO SUPER_ADMIN is auto-granted Django is_staff/is_superuser (previous versions promoted everyone in that case). Set at least one to retain auto-promotion; otherwise provision admins manually. A startup WARNING is 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_FALLBACK is unset or False, a JWKS resolution failure results in AuthenticationFailed (401) instead of silently retrying against OUA_PUBLIC_KEY. Clients that relied on the implicit fallback must set OUA_ALLOW_STATIC_KEY_FALLBACK = True explicitly — 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

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 user
  • request.oua_claims — decoded JWT payload
  • request.oua_tenant — dict with country_id, company_id, branch_id, sso_role, role_level
  • request.tenant_scopeTenantScope dataclass (set by TenantScopeMiddleware)

Browser Login (OAuth2 + PKCE)

For web apps that need to redirect users to the SSO login page:

  1. User visits a protected page
  2. @sso_login_required redirects to /sso/login/
  3. OUA generates PKCE challenge and redirects to SSO authorize endpoint
  4. User authenticates on SSO (with optional 2FA)
  5. SSO redirects back to /sso/callback/ with an authorization code
  6. OUA exchanges the code for tokens, creates a Django session
  7. 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 modelSUPER_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


Download files

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

Source Distribution

oua_auth-1.0.0.tar.gz (134.1 kB view details)

Uploaded Source

Built Distribution

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

oua_auth-1.0.0-py3-none-any.whl (52.5 kB view details)

Uploaded Python 3

File details

Details for the file oua_auth-1.0.0.tar.gz.

File metadata

  • Download URL: oua_auth-1.0.0.tar.gz
  • Upload date:
  • Size: 134.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.13

File hashes

Hashes for oua_auth-1.0.0.tar.gz
Algorithm Hash digest
SHA256 5dd5cb0bc382b254a54061ae413c58d6ad5a6a4aa2fea30a235493cc36259384
MD5 bfdbd8979223bd720eb3b3284e2411de
BLAKE2b-256 61c352fd3c96d0b852c0d6c47e2eddfa863aaeaf73352a4f5e2c88c6a55591c8

See more details on using hashes here.

File details

Details for the file oua_auth-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: oua_auth-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 52.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.13

File hashes

Hashes for oua_auth-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a365670e9cb26e1ef405fe3500e3e0b36864b404a9fcb3953c09f1c579d26aa8
MD5 149dcd23e7e8d6c6a4da2d5727682f67
BLAKE2b-256 ae0f19fe34295b0eb5b97b5e7c6ff998fc99eb37e42fa00e3325bb231a3d876d

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