Metadata-Version: 2.4
Name: crimsonforge
Version: 2.5.7.dev0
Summary: Ed25519 JWT authentication library for CrimsonForge backends
Author-email: CrimsonForge Team <dev@crimsonforge.com>
License-Expression: PolyForm-Noncommercial-1.0.0
Project-URL: Homepage, https://github.com/CrimsonForge-io/crimsonforge-library
Project-URL: Documentation, https://github.com/CrimsonForge-io/crimsonforge-library#readme
Project-URL: Repository, https://github.com/CrimsonForge-io/crimsonforge-library.git
Project-URL: Issues, https://github.com/CrimsonForge-io/crimsonforge-library/issues
Keywords: django,jwt,authentication,authorization,crimsonforge
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: Django>=5.2.17
Requires-Dist: PyJWT>=2.15.1
Requires-Dist: cryptography>=50.0.2
Requires-Dist: requests>=2.33.0
Requires-Dist: redis>=5.0.0
Provides-Extra: drf
Requires-Dist: djangorestframework>=3.14; extra == "drf"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: pytest-django>=4.5.0; extra == "dev"
Requires-Dist: djangorestframework>=3.14; extra == "dev"
Requires-Dist: fakeredis[lua]>=2.23; extra == "dev"
Dynamic: license-file

# CrimsonForge Library

A reusable Django library for validating JWT tokens issued by the CrimsonForge Web App backend using Ed25519 public key verification.

Public source for noncommercial use under [PolyForm Noncommercial 1.0.0](LICENSE).
Commercial use requires a separate written license from Crimson Forge, LLC.

## Features

- **Ed25519 JWT Verification**: Secure asymmetric signature verification using public keys
- **Automatic Key Fetching**: Public keys are automatically fetched from the WA backend
- **Key Caching**: Public keys are cached to minimize API calls
- **Key Rotation Support**: Automatic handling of key rotation via `kid` header
- **Django Middleware**: Automatic authentication on all requests
- **View Decorators**: Easy authentication and permission enforcement
- **Team Isolation**: Built-in support for multi-tenant applications
- **Proof of Possession**: Browser handshake and per-request proofs backed by Redis

## Installation

Requires Python 3.10 or newer and Django 5.2.17 or newer. Django REST Framework
integrations use the optional `drf` extra. PoP requires a Redis service.

### From Source

```bash
git clone --branch main https://github.com/CrimsonForge-io/crimsonforge-library.git
cd crimsonforge-library
python -m pip install .
```

### From PyPI (After Publication)

Version 2.5.6 is prepared for review; it has not been published yet. Once published:

```bash
python -m pip install crimsonforge==2.5.6
```

For Django REST Framework integrations, install `crimsonforge[drf]==2.5.6`.
Until publication, the built wheel can also be installed directly with pip.

Stable releases come from `main`; development releases come from `dev` and use
versions such as `2.5.7.dev0`. Pin the exact version in each deployment. To
enforce the selection at Django startup, configure:

```bash
# Production
CRIMSONFORGE_LIBRARY_CHANNEL=main
CRIMSONFORGE_LIBRARY_VERSION=2.5.6

# Development (with crimsonforge==2.5.7.dev0 installed)
CRIMSONFORGE_LIBRARY_CHANNEL=dev
CRIMSONFORGE_LIBRARY_VERSION=2.5.7.dev0
```

Set one pair for the environment. A wrong channel or version stops Django
startup. These settings select the library release; configure the matching
CrimsonForge webapp API URL separately. Existing integrations can omit them
until they adopt release pinning.

## Quick Start

### 1. Configure Django Settings

Add the middleware and configure the WA backend URL:

```python
# settings.py

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # Authenticate before verifying proof of possession
    'crimsonforge.auth.JWTAuthMiddleware',
    'crimsonforge.pop.PoPAuthMiddleware',
    # ... other middleware
]

# CrimsonForge configuration
CRIMSONFORGE = {
    'team_id': 'your-team-uuid',  # Required - get from Settings > Backend Configuration
    'service_name': 'king_phisher',  # Your service name
    'exempt_paths': ['/health/', '/status/'],  # Paths without auth
    'require_auth': True,  # Return 401 for missing tokens
    'pop_redis_url': 'redis://localhost:6379/0',
    # 'wa_backend_url': 'https://api.crimsonforge.io/api',  # Default, override if needed
    # 'public_key_cache_ttl': 3600,  # Cache public key for 1 hour (default)
}

# Or use environment variables:
# CRIMSONFORGE_TEAM_ID=your-team-uuid  (Required)
# CRIMSONFORGE_SERVICE_NAME=king_phisher
# CRIMSONFORGE_WA_BACKEND_URL=https://api.crimsonforge.io/api  (optional, this is the default)
# CRIMSONFORGE_SERVICE_CLIENT_ID=credential-uuid  (for machine-initiated WA calls)
# CRIMSONFORGE_SERVICE_CLIENT_SECRET=show-once-secret
# CRIMSONFORGE_POP_REDIS_URL=redis://localhost:6379/0
```

Add `crimsonforge` to `INSTALLED_APPS` and run `python manage.py migrate` when
integrating it into an existing Django application. The package includes its
historical migrations, including removal of the retired local team-key cache.

Your application must expose its PoP handshake POST route and protect it with
JWT authentication while exempting it from the per-request PoP proof check.
See the existing KP integration for application routing. The library supplies
handshake helpers; installing it does not add application URL routes.

Machine-initiated calls use the client credential only at the WA backend's
token exchange. The library caches the returned 15-minute JWT, renews it up to
60 seconds early, briefly backs off when an early refresh fails, and performs
one coordinated forced refresh after a 401. The durable client secret is never
sent to application endpoints.

### 2. Protect Your Views

Use decorators to enforce authentication:

```python
# views.py

from django.http import JsonResponse
from crimsonforge.decorators import require_authenticated, require_admin

@require_authenticated
def list_campaigns(request):
    """List all campaigns for the authenticated team."""
    team_id = request.team_id  # Automatically set by middleware
    user_id = request.user_id
    email = request.email

    campaigns = Campaign.objects.filter(team_id=team_id)
    return JsonResponse({
        'campaigns': [c.to_dict() for c in campaigns]
    })

@require_admin
def admin_settings(request):
    """Admin-only endpoint."""
    # Only users with team_role='admin' can access
    return JsonResponse({'settings': {...}})
```

### 3. Access Token Data in Views

The middleware attaches token data to every authenticated request:

```python
def my_view(request):
    if request.is_authenticated:
        user_id = request.user_id
        team_id = request.team_id
        email = request.email

        # Access full token for more details
        token = request.token
        is_admin = token.is_admin()
        mfa_verified = token.mfa_verified
```

## Architecture

### Token Flow

1. User authenticates via WebAuthn on the WA backend
2. WA backend issues JWT signed with Ed25519 private key
3. Frontend stores JWT and sends with all API requests
4. Your backend validates JWT using public key from WA backend
5. Public key is cached for performance (1 hour TTL)
6. Key rotation is handled automatically via `kid` header

### JWT Token Structure

```json
{
  "alg": "EdDSA",
  "typ": "JWT",
  "kid": "key-id-uuid"
}
{
  "sub": "user-uuid",
  "email": "user@example.com",
  "team_id": "team-uuid",
  "team_role": "admin",
  "mfa": true,
  "exp": 1234567890,
  "iat": 1234565090
}
```

## API Reference

### Middleware

#### `JWTAuthMiddleware`

Validates tokens and attaches authentication data to request:

- `request.token` - AccessToken object (or None)
- `request.team_id` - Team ID from token (or None)
- `request.user_id` - User ID from token (or None)
- `request.email` - Email from token (or None)
- `request.is_authenticated` - Boolean

**Configuration options:**
- `team_id` - Team ID this backend serves (required)
- `wa_backend_url` - URL of WA backend (default: `https://api.crimsonforge.io/api`)
- `exempt_paths` - List of paths to skip auth check
- `require_auth` - If True, return 401 for missing tokens
- `public_key_cache_ttl` - Cache TTL in seconds (default: 3600)

### Decorators

#### `@require_authenticated`

Requires a valid authentication token.

```python
@require_authenticated
def my_view(request):
    ...
```

#### `@require_admin`

Requires admin team role.

```python
@require_admin
def admin_view(request):
    ...
```

#### `@require_permission(permission)`

Requires a specific permission string.

#### `@require_any_permission(permissions)`

Requires at least one permission from a list.

#### `@require_all_permissions(permissions)`

Requires all permissions from a list.

### Token Validation

#### `validate_token(token_string)`

Manually validate a JWT token:

```python
from crimsonforge.tokens import validate_token

try:
    token = validate_token(jwt_string)
    print(f"User: {token.user_id}, Team: {token.team_id}")
except TokenValidationError as e:
    print(f"Invalid token: {e}")
```

### Settings Access

#### `settings.fetch_jwt_public_key(force_refresh=False)`

Fetch the public key from WA backend:

```python
from crimsonforge.settings import settings

key_data = settings.fetch_jwt_public_key()
# Returns: {'public_key': '-----BEGIN...', 'algorithm': 'EdDSA', 'kid': 'uuid'}
```

## Testing

Run the test suite:

```bash
cd crimsonforge-library
python -m pip install -e ".[dev]"
python -m pytest
```

Or with coverage:

```bash
python -m pytest --cov=crimsonforge --cov-report=html
```

## Security Considerations

- **Public Key Verification**: Tokens are verified using Ed25519 public keys fetched from the WA backend
- **No Shared Secrets**: Unlike HS256, the private key never leaves the WA backend
- **Key Rotation**: Keys are rotated automatically; the library handles this via `kid` matching
- **Team ID Validation**: The middleware validates that the JWT's team_id matches the configured team_id
- **HTTPS Only**: Always use HTTPS in production
- **Token Expiration**: Tokens expire after 15 minutes (enforced by JWT exp claim)
- **Machine Credential Isolation**: Durable client secrets are hashed at rest and exchanged only for short-lived, audience-bound access JWTs
- **Team Isolation**: The middleware enforces team isolation; only users from the configured team can access

### About Team ID

The `team_id` configuration is not a secret. It simply identifies which team this backend instance serves. Think of it as a filter that ensures:

1. Users from other teams cannot access your backend (even with valid JWTs)
2. Each backend instance is explicitly bound to one team

Get your team ID from the CrimsonForge web app at **Settings > Backend Configuration**.

## License

Copyright (c) 2026 Crimson Forge, LLC. Licensed under
[PolyForm Noncommercial 1.0.0](LICENSE). Use, modification, and distribution
are permitted for the purposes specified by that license. Commercial use
requires a separate written license. Preserve [NOTICE](NOTICE) and the license
when distributing copies or modifications.

See [the release guide](docs/releasing.md) for building packages and the manual
TestPyPI/PyPI publishing workflow. Publication requires explicit release action.
