# Custom Authentication Example Plugin
This plugin demonstrates the two-layer HTTP authentication hook architecture in MCP Gateway, showing how to implement custom authentication mechanisms.
## Overview
The plugin showcases both authentication layers:
1. **HTTP_PRE_REQUEST** (Middleware Layer): Transform custom authentication headers before authentication
2. **HTTP_AUTH_RESOLVE_USER** (Auth Layer): Implement custom user authentication systems
## Use Cases
- Convert custom API key headers to standard bearer tokens
- Authenticate users via LDAP/Active Directory
- Validate mTLS client certificates
- Integrate with external authentication services
- Support proprietary token formats
## Configuration
Add to `plugins/config.yaml`:
```yaml
plugins:
- name: custom_auth_example
enabled: true
priority: 10
config:
# Custom header to extract API key from (case-insensitive)
api_key_header: "x-api-key"
# Mapping of API keys to user information
# Key: API key value
# Value: User information dict
api_key_mapping:
"demo-key-12345":
email: "demo@example.com"
full_name: "Demo User"
is_admin: false
"admin-key-67890":
email: "admin@example.com"
full_name: "Admin User"
is_admin: true
# Enable LDAP authentication (placeholder for demonstration)
ldap_enabled: false
# Enable mTLS certificate authentication
mtls_enabled: false
# Transform custom headers to Authorization: Bearer format
transform_headers: true
```
## How It Works
### Layer 1: Header Transformation (HTTP_PRE_REQUEST)
Runs in middleware **before** any authentication logic:
```
Client Request:
X-API-Key: demo-key-12345
↓ HTTP_PRE_REQUEST hook transforms headers
Modified Request:
Authorization: Bearer demo-key-12345
X-API-Key: demo-key-12345 (original preserved)
```
This allows clients to use custom authentication headers that get transformed into standard formats.
### Layer 2: User Authentication (HTTP_AUTH_RESOLVE_USER)
Runs inside `get_current_user()` **before** standard JWT validation:
```
1. Plugin receives authentication payload:
- credentials: {"scheme": "Bearer", "credentials": "demo-key-12345"}
- headers: All request headers
- client_host: "192.168.1.100"
- client_port: 54321
2. Plugin checks API key mapping:
- Finds "demo-key-12345" in api_key_mapping
- Retrieves user info: {"email": "demo@example.com", ...}
3. Plugin returns authenticated user:
- User object created from mapping
- continue_processing = False (skip standard JWT auth)
4. If no match, plugin returns continue_processing = True:
- Falls back to standard JWT/API token validation
```
## Usage Examples
### Example 1: API Key Authentication
**Client Request:**
```bash
curl -H "X-API-Key: demo-key-12345" \
https://gateway.example.com/protocol/initialize
```
**What Happens:**
1. Middleware transforms `X-API-Key` → `Authorization: Bearer demo-key-12345`
2. Auth resolution hook looks up `demo-key-12345` in `api_key_mapping`
3. User `demo@example.com` is authenticated
4. Request succeeds with user context
### Example 2: Standard Bearer Token (Fallback)
**Client Request:**
```bash
curl -H "Authorization: Bearer eyJhbGciOi..." \
https://gateway.example.com/protocol/initialize
```
**What Happens:**
1. Middleware sees standard Authorization header, no transformation needed
2. Auth resolution hook doesn't find token in API key mapping
3. Returns `continue_processing=True`
4. Falls back to standard JWT validation
5. Request succeeds if JWT is valid
### Example 3: mTLS Certificate Authentication
**Configuration:**
```yaml
config:
mtls_enabled: true
```
**Nginx/Reverse Proxy Configuration:**
```nginx
location / {
proxy_pass http://mcp-gateway:4444;
proxy_set_header X-Client-Cert-DN $ssl_client_s_dn;
ssl_client_certificate /path/to/ca.crt;
ssl_verify_client on;
}
```
**What Happens:**
1. Client presents TLS client certificate
2. Reverse proxy validates certificate
3. Proxy adds `X-Client-Cert-DN: CN=user@example.com,O=Example` header
4. Plugin extracts DN and authenticates user
5. Request succeeds with user context from certificate
### Example 4: Multiple Authentication Methods
The plugin supports fallback chains:
```
Priority 1: API Key Mapping
↓ (if not found)
Priority 2: mTLS Certificate
↓ (if not enabled/found)
Priority 3: LDAP Token
↓ (if not enabled/found)
Fallback: Standard JWT/API Token Validation
```
## Security Considerations
1. **API Key Storage**: Store API keys securely, never commit to version control
2. **Environment Variables**: Use environment variable substitution in config:
```yaml
api_key_mapping:
"${DEMO_API_KEY}":
email: "demo@example.com"
```
3. **Rate Limiting**: Combine with rate_limiter plugin to prevent brute force
4. **Audit Logging**: Plugin logs authentication attempts at INFO level
5. **Token Rotation**: Regularly rotate API keys in production
6. **mTLS Security**: Validate certificate revocation status in production
## Extending the Plugin
### Add LDAP Authentication
```python
async def http_auth_resolve_user(self, payload, context):
if self._cfg.ldap_enabled:
ldap_token = payload.headers.root.get("x-ldap-token")
if ldap_token:
# Import LDAP library
import ldap3
# Connect to LDAP server
server = ldap3.Server(self._cfg.ldap_server)
conn = ldap3.Connection(server, user=dn, password=ldap_token)
# Authenticate
if conn.bind():
# Query user attributes
conn.search(...)
user_info = conn.entries[0]
# Return authenticated user
return PluginResult(
modified_payload={
"email": user_info.mail.value,
"full_name": user_info.displayName.value,
...
},
continue_processing=False
)
return PluginResult(continue_processing=True)
```
### Add OAuth Token Validation
```python
async def http_auth_resolve_user(self, payload, context):
if payload.credentials:
token = payload.credentials.get("credentials")
# Validate with external OAuth provider
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://oauth.provider.com/userinfo",
headers={"Authorization": f"Bearer {token}"}
)
if resp.status_code == 200:
user_info = resp.json()
return PluginResult(
modified_payload={
"email": user_info["email"],
"full_name": user_info["name"],
...
},
continue_processing=False
)
return PluginResult(continue_processing=True)
```
## Testing
Create test cases in `tests/unit/mcpgateway/plugins/plugins/custom_auth_example/`:
```python
import pytest
from mcpgateway.plugins.framework import (
HttpAuthResolveUserPayload,
HttpHeaderPayload,
HttpPreRequestPayload,
PluginConfig,
PluginContext,
)
from mcpgateway.plugins.custom_auth_example.custom_auth import CustomAuthPlugin
@pytest.fixture
def plugin():
config = PluginConfig(
name="custom_auth",
config={
"api_key_mapping": {
"test-key-123": {
"email": "test@example.com",
"full_name": "Test User",
"is_admin": False,
}
}
}
)
return CustomAuthPlugin(config)
@pytest.mark.asyncio
async def test_header_transformation(plugin):
"""Test X-API-Key → Authorization transformation."""
payload = HttpPreRequestPayload(
path="/protocol/initialize",
method="POST",
headers=HttpHeaderPayload({"x-api-key": "test-key-123"}),
)
context = PluginContext(request_id="test-123")
result = await plugin.http_pre_request(payload, context)
assert result.modified_payload is not None
assert result.modified_payload.root["authorization"] == "Bearer test-key-123"
@pytest.mark.asyncio
async def test_api_key_authentication(plugin):
"""Test API key lookup and user authentication."""
payload = HttpAuthResolveUserPayload(
credentials={"scheme": "Bearer", "credentials": "test-key-123"},
headers=HttpHeaderPayload({}),
)
context = PluginContext(request_id="test-456")
result = await plugin.http_auth_resolve_user(payload, context)
assert result.modified_payload is not None
assert result.modified_payload["email"] == "test@example.com"
assert result.continue_processing is False
```
## Integration with Authorization Flow
This plugin integrates with MCP Gateway's authentication flow as documented in `docs/docs/architecture/authorization-flow.md`:
```
Client Request
↓
[1] HTTP_PRE_REQUEST Hook (this plugin)
- Transform X-API-Key to Authorization header
↓
[2] TokenScopingMiddleware
- Check IP/time restrictions
↓
[3] Route Handler with get_current_user()
↓
[3a] HTTP_AUTH_RESOLVE_USER Hook (this plugin)
- Check API key mapping
- Authenticate user via custom method
↓ (if continue_processing=False)
User Authenticated by Plugin
↓
[3b] Standard JWT Validation (skipped)
↓
[4] RBAC Permission Checks
↓
Response
```
## Troubleshooting
### API Key Not Working
1. Check plugin is enabled in `plugins/config.yaml`
2. Verify API key is in `api_key_mapping`
3. Check header name matches `api_key_header` (case-insensitive)
4. Review logs: `grep "CustomAuthPlugin" logs/mcpgateway.log`
### Headers Not Transformed
1. Ensure `transform_headers: true` in config
2. Verify Authorization header is not already present
3. Check plugin priority (should run early, priority < 50)
### Authentication Falls Back to JWT
This is expected behavior when:
- API key not found in mapping
- Custom auth method not enabled
- Plugin returns `continue_processing=True`
The gateway will try standard JWT/API token validation as fallback.
## References
- [Authorization Flow Documentation](../../docs/docs/architecture/authorization-flow.md)
- [Plugin Framework Documentation](../../docs/docs/architecture/plugins.md)
- [HTTP Authentication Hooks](../../mcpgateway/plugins/framework/hooks/http.py)
- [Auth Middleware](../../mcpgateway/middleware/http_auth_middleware.py)
- [get_current_user() Implementation](../../mcpgateway/auth.py)