# Security Guide

This guide covers security best practices for Django MCP servers.

## Fundamental Principle: "Agent as User"

Django MCP treats AI agents as authenticated users. Every MCP tool invocation:

1. **Authenticates** the agent (via bearer token, OAuth2, etc.)
2. **Binds** the agent to a Django User or AnonymousUser
3. **Enforces** all DRF permission checks
4. **Audits** the invocation

**You cannot bypass DRF's permission system in Django MCP.**

## Authentication

### Default: No Authentication

By default, Django MCP runs without authentication. Only use this for:
- Local development
- Private networks with network-level security
- Testing and prototyping

### Production: OAuth2/OIDC

For production, implement token-based authentication:

#### 1. Install OAuth2 Toolkit

```bash
uv add django-oauth-toolkit
```

#### 2. Configure Custom Authentication

```python
# settings.py
INSTALLED_APPS = [
    ...
    'oauth2_provider',
    'rest_framework',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
    ],
}
```

#### 3. Create Agent Clients

```bash
python manage.py creatersakey
python manage.py create_oauth2_client \
    --name "claude-agent" \
    --client-type confidential \
    --grant-type client-credentials
```

#### 4. Agents Use Token

```bash
curl -X POST https://your-api.com/o/token/ \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "grant_type=client_credentials"

# Returns: {"access_token": "...", "token_type": "Bearer"}
```

#### 5. MCP Server Validates Token

```python
# mcp_server.py
from drf_mcp.auth import MCPTokenAuthentication

# Token validation happens automatically in ViewInvoker
# when DRF permission checks run
```

### OIDC / OpenID Connect

For federated identity (e.g., corporate SSO):

```bash
uv add django-oidc-auth
```

```python
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'oidc_auth.authentication.BearerTokenAuthentication',
    ],
}

OIDC_RP_CLIENT_ID = "your-client-id"
OIDC_RP_CLIENT_SECRET = "your-client-secret"
OIDC_OP_AUTHORIZATION_ENDPOINT = "https://your-identity-provider.com/oauth/authorize"
OIDC_OP_TOKEN_ENDPOINT = "https://your-identity-provider.com/oauth/token"
OIDC_OP_USER_ENDPOINT = "https://your-identity-provider.com/oauth/userinfo"
```

## Permission Classes

Your DRF permission classes automatically apply to MCP tools:

```python
from rest_framework.permissions import (
    IsAuthenticated,
    IsAdminUser,
    BasePermission,
)

class IsTeamMember(BasePermission):
    """Only team members can access"""
    def has_permission(self, request, view):
        return request.user.groups.filter(name='team').exists()

class CustomerViewSet(viewsets.ModelViewSet):
    serializer_class = CustomerSerializer
    permission_classes = [IsAuthenticated, IsTeamMember]
    
    # Tools derived from this ViewSet enforce permissions
```

## Row-Level Security (Object-Level Permissions)

Restrict access to specific records:

```python
from rest_framework.permissions import BasePermission

class IsCustomerOwner(BasePermission):
    """Only access your own customer records"""
    def has_object_permission(self, request, view, obj):
        return obj.owner == request.user

class CustomerViewSet(viewsets.ModelViewSet):
    serializer_class = CustomerSerializer
    permission_classes = [IsAuthenticated, IsCustomerOwner]
    
    def get_queryset(self):
        # Filter to user's customers
        return Customer.objects.filter(owner=self.request.user)

# When MCP agent calls retrieve_customer(id=123),
# has_object_permission() checks if customer.owner == agent's user
```

## Scope-Based Access (Fine-Grained Permissions)

Use OAuth2 scopes to grant agents specific permissions:

```python
# settings.py
OAUTH2_SCOPES = {
    'customers:read': 'Read customer data',
    'customers:write': 'Modify customer data',
    'customers:delete': 'Delete customers',
}

# Grant agents different scopes:
# - Marketing agent: customers:read
# - Sales agent: customers:read + customers:write
# - Admin agent: customers:*
```

```python
# views.py
from rest_framework.decorators import action
from rest_framework.permissions import BasePermission

class HasCustomerReadScope(BasePermission):
    def has_permission(self, request, view):
        scopes = request.auth.scopes if request.auth else []
        return 'customers:read' in scopes

class CustomerViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, HasCustomerReadScope]
```

## Audit Logging

Log all MCP tool invocations:

```python
# settings.py
LOGGING = {
    'version': 1,
    'handlers': {
        'mcp_audit': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': 'mcp_audit.log',
        },
    },
    'loggers': {
        'drf_mcp': {
            'handlers': ['mcp_audit'],
            'level': 'INFO',
        },
    },
}
```

Audit trail includes:
- Agent/User ID
- Tool name
- Arguments (redacted for sensitive data)
- Timestamp
- Result (success/denied/error)
- IP address (if available)

## Transport Security

### Stdio (Local)

Secure by default for local development. Claude Desktop runs on same machine.

### HTTP

**Always use HTTPS in production:**

```python
# settings.py
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
```

**CORS Configuration:**

```bash
uv add django-cors-headers
```

```python
# settings.py
INSTALLED_APPS = [
    'corsheaders',
    ...
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    ...
]

CORS_ALLOWED_ORIGINS = [
    "https://trusted-client.example.com",
]

CORS_ALLOW_CREDENTIALS = True
```

### Stdio over SSH

For remote execution, tunnel via SSH:

```bash
ssh user@remote-server \
  -L 8000:localhost:8000 \
  "cd /path && python mcp_server.py --transport http --host 127.0.0.1"
```

## Data Protection

### Sensitive Fields

Exclude sensitive data from serializer schemas:

```python
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'name', 'email']  # Don't include 'password'
        # or use SerializerMethodField for computed data
```

### Field-Level Redaction

```python
from django_filters import BaseInFilter

class RedactedCharFilter(BaseInFilter, filters.CharFilter):
    """Custom filter that redacts sensitive data"""
    pass

class SensitiveDataViewSet(viewsets.ModelViewSet):
    filterset_fields = {
        'ssn': [RedactedCharFilter()],  # Hide SSN in logs
    }
```

### Read-Only Fields

Mark sensitive fields as read-only:

```python
class CustomerSerializer(serializers.ModelSerializer):
    # Agents can read but not modify
    created_at = serializers.DateTimeField(read_only=True)
    last_modified_by = serializers.StringRelatedField(read_only=True)
    
    class Meta:
        model = Customer
        fields = ['id', 'name', 'email', 'created_at', 'last_modified_by']
```

## Rate Limiting

Prevent abuse via rate limiting:

```bash
uv add djangorestframework-simplejwt
```

```python
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/hour',
        'user': '1000/hour',  # Per-agent limit
    },
}
```

## Secrets Management

**Never commit secrets to git:**

```python
# ✗ WRONG:
PASSWORD = "hardcoded-secret"

# ✓ CORRECT:
import os
from decouple import config

PASSWORD = config('OAUTH2_SECRET', default=None)
```

```bash
# .env (add to .gitignore)
OAUTH2_SECRET=your-secret-here
OIDC_RP_CLIENT_SECRET=your-oidc-secret
```

## Compliance

### GDPR

Implement data deletion for agents:

```python
# views.py
class CustomerViewSet(viewsets.ModelViewSet):
    @action(detail=True, methods=['delete'])
    def gdpr_delete(self, request, pk=None):
        """Securely delete customer data"""
        customer = self.get_object()
        # Check permissions
        self.check_object_permissions(request, customer)
        # Delete with audit log
        customer.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
```

### SOC 2

Maintain audit logs:

```bash
uv add django-auditlog
```

```python
from auditlog.registry import auditlog

class Customer(models.Model):
    ...

auditlog.register(Customer)
```

## Security Checklist

- [ ] Agents authenticate before tool access
- [ ] DRF permission classes enforced on all tools
- [ ] HTTPS only in production
- [ ] CORS properly configured
- [ ] Sensitive fields marked read-only
- [ ] Rate limiting enabled
- [ ] Audit logging configured
- [ ] OAuth2 scopes implemented
- [ ] Secrets in environment variables
- [ ] Regular security audits scheduled

## Next Steps

- [Getting Started](./getting_started.md) - Implement basic auth
- [Architecture](./architecture.md) - Understand permission flow
- [API Reference](../api/) - Detailed security APIs
