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

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:

  1. Subclass AbstractSCIMIdentity in your own app.
  2. Set the subclass's Meta.db_table to your existing identity table name.
  3. Point OUA_SCIM_IDENTITY_MODEL at that subclass. Like AUTH_USER_MODEL, the swap is resolved at app-registry load, so set this before startup (and before running migrations).
  4. Generate the re-base migration and verify it is state-only — no CREATE TABLE / ALTER TABLE against the live table (use a SeparateDatabaseAndState / --state-only migration). 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 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.5.0.tar.gz (169.4 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.5.0-py3-none-any.whl (75.5 kB view details)

Uploaded Python 3

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

Hashes for oua_auth-1.5.0.tar.gz
Algorithm Hash digest
SHA256 2d04e3746f7c760d07fe299acfd503a16beb6ebb05504654dd91b96227d1dd62
MD5 7da525f7f80f49b883927e9cb7fdd821
BLAKE2b-256 a5becb32c02945ce630d62a40fa48afa64a4842c43e59447f055312991b0701e

See more details on using hashes here.

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

Hashes for oua_auth-1.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bac885b1234f66bc2ea47480fcad86ee0749a6922403e0828691233336e05c15
MD5 892c7486b3bac17864e71054b41b4406
BLAKE2b-256 83a032cd1e27dbfc9f3a36d89e113310a251c076f60912122389a9def961fbaa

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