Metadata-Version: 2.4
Name: oua-auth
Version: 0.5.0
Summary: A Django authentication middleware package for integrating with OUA SSO server
Home-page: https://gitlab.com/lexnjugz/Organization_Unified_Access_Authentication
Author: Organization Unified Access
Author-email: alexmungai964@gmail.com
Project-URL: Source, https://gitlab.com/lexnjugz/Organization_Unified_Access_Authentication
Project-URL: Tracker, https://gitlab.com/lexnjugz/Organization_Unified_Access_Authentication/-/issues
Project-URL: Documentation, https://organization-unified-access-authentication.readthedocs.io/en/latest
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 3.2
Classifier: Framework :: Django :: 4.0
Classifier: Framework :: Django :: 4.1
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=3.2
Requires-Dist: PyJWT[crypto]>=2.8.0
Requires-Dist: bleach>=5.0.0
Requires-Dist: requests>=2.25.0
Requires-Dist: djangorestframework>=3.12.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-django>=4.7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
Requires-Dist: black>=24.2.0; extra == "dev"
Requires-Dist: isort>=5.12.0; extra == "dev"
Requires-Dist: flake8>=7.0.0; extra == "dev"
Requires-Dist: build>=1.0.3; extra == "dev"
Requires-Dist: twine>=5.0.0; extra == "dev"
Provides-Extra: test
Requires-Dist: pytest>=8.0.0; extra == "test"
Requires-Dist: pytest-django>=4.7.0; extra == "test"
Requires-Dist: pytest-cov>=4.1.0; extra == "test"
Requires-Dist: pytest-mock>=3.14.0; extra == "test"
Requires-Dist: coverage>=7.2.0; extra == "test"
Provides-Extra: docs
Requires-Dist: mkdocs>=1.5.0; extra == "docs"
Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
Requires-Dist: markdown>=3.5.0; extra == "docs"
Requires-Dist: pymdown-extensions>=10.0.0; extra == "docs"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license-file
Dynamic: project-url
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# 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

```bash
pip install oua-auth
```

## Quick Start

```python
# 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:

```bash
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: `/`) |

### Security Settings

| Setting | Default | Description |
|---------|---------|-------------|
| `OUA_PUBLIC_KEY` | `None` | Static PEM fallback when JWKS unavailable |
| `OUA_TOKEN_LEEWAY` | `0` | Clock skew tolerance in seconds |
| `OUA_TRUSTED_ADMIN_DOMAINS` | `[]` | Domains allowed Django admin access |
| `OUA_TRUSTED_ADMIN_EMAILS` | `[]` | Emails allowed Django admin access |
| `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_scope` — `TenantScope` 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

```python
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

```python
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

```python
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

```python
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:

```bash
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 bleach
- **Security headers middleware** — CSP, HSTS, X-Frame-Options, etc.
- **Structured logging** — JSON output with sensitive data redaction

## Testing

```bash
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.
