Add wiki page: Authentication-and-Security
@@ -0,0 +1,270 @@
|
||||
# Authentication and Security
|
||||
|
||||
This page covers the full authentication model, token management, API key usage,
|
||||
SSO configuration, and security controls in Aegis.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
Aegis supports two authentication methods, checked in order:
|
||||
|
||||
### 1. HttpOnly Cookie (Primary — Browser Use)
|
||||
|
||||
After a successful login, the server sets:
|
||||
|
||||
```
|
||||
Set-Cookie: aegis_token=<JWT>; HttpOnly; SameSite=Strict; Path=/; [Secure in prod]
|
||||
```
|
||||
|
||||
The browser automatically sends this cookie on every request.
|
||||
**This is the recommended method for the frontend.**
|
||||
|
||||
### 2. Authorization: Bearer Header (API / Machine Use)
|
||||
|
||||
```http
|
||||
Authorization: Bearer <JWT>
|
||||
```
|
||||
|
||||
Use this for:
|
||||
- API integrations
|
||||
- Scripts and CI/CD pipelines
|
||||
- When cookies are not available (e.g. cross-origin API calls)
|
||||
|
||||
---
|
||||
|
||||
## JWT Token
|
||||
|
||||
### Structure
|
||||
|
||||
The JWT payload contains:
|
||||
```json
|
||||
{
|
||||
"sub": "user-uuid",
|
||||
"role": "red_lead",
|
||||
"jti": "unique-token-id",
|
||||
"exp": 1710000000,
|
||||
"iat": 1709996400
|
||||
}
|
||||
```
|
||||
|
||||
### Expiry
|
||||
|
||||
Configurable via environment variable:
|
||||
```
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=480 # 8 hours default
|
||||
```
|
||||
|
||||
### Token Revocation (Blacklist)
|
||||
|
||||
On logout, the token's `jti` is stored in Redis with TTL = remaining token lifetime:
|
||||
```
|
||||
Redis key: blacklist:{jti}
|
||||
Value: "1"
|
||||
TTL: <seconds until token expiry>
|
||||
```
|
||||
|
||||
Every authenticated request checks Redis for the `jti`. If found → 401 Unauthorized.
|
||||
|
||||
---
|
||||
|
||||
## Cookie Security
|
||||
|
||||
| Setting | Development | Production |
|
||||
|---------|-------------|------------|
|
||||
| `HttpOnly` | True | True |
|
||||
| `SameSite` | Strict | Strict |
|
||||
| `Secure` | False | True |
|
||||
| `Path` | / | / |
|
||||
|
||||
**`SECURE_COOKIES` environment variable:**
|
||||
- `auto` (default): `Secure=True` when `AEGIS_ENV=production`
|
||||
- `true`: Always set `Secure` flag
|
||||
- `false`: Never set `Secure` flag (development only)
|
||||
|
||||
---
|
||||
|
||||
## Password Policy
|
||||
|
||||
- Minimum length: **12 characters**
|
||||
- Must contain: uppercase, lowercase, digit, and special character
|
||||
- Enforced at account creation and password change
|
||||
- Passwords are hashed with **bcrypt** (work factor 12)
|
||||
|
||||
### must_change_password
|
||||
|
||||
When an admin creates a user, `must_change_password` is set to `True`.
|
||||
Until the user changes their password, **every endpoint** returns:
|
||||
```json
|
||||
{"detail": "PASSWORD_CHANGE_REQUIRED"}
|
||||
```
|
||||
with status `403`, **except**:
|
||||
- `GET /api/v1/auth/me`
|
||||
- `POST /api/v1/auth/change-password`
|
||||
|
||||
Change password:
|
||||
```http
|
||||
POST /api/v1/auth/change-password
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"current_password": "admin-set-password",
|
||||
"new_password": "MyNewSecurePassword123!"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Keys
|
||||
|
||||
API keys provide long-lived credentials for machine-to-machine access without
|
||||
session management.
|
||||
|
||||
### Creating a Key
|
||||
|
||||
```http
|
||||
POST /api/v1/api-keys
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "CI Pipeline Key",
|
||||
"scope": "read",
|
||||
"expires_at": "2025-12-31T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Response includes the key value (only shown once):
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "CI Pipeline Key",
|
||||
"key": "aegis_a1b2c3d4e5f6...",
|
||||
"scope": "read"
|
||||
}
|
||||
```
|
||||
|
||||
### Using a Key
|
||||
|
||||
```http
|
||||
Authorization: Bearer aegis_a1b2c3d4e5f6...
|
||||
```
|
||||
|
||||
### Scopes
|
||||
|
||||
| Scope | Allowed HTTP methods |
|
||||
|-------|---------------------|
|
||||
| `read` | GET, HEAD, OPTIONS only |
|
||||
| `write` | GET, POST, PATCH, PUT, DELETE |
|
||||
| `admin` | All + admin-only endpoints |
|
||||
|
||||
A `read` scope key that attempts POST/PATCH/DELETE receives:
|
||||
```json
|
||||
{"detail": "API key scope 'read' does not permit this operation"}
|
||||
```
|
||||
with status `403`.
|
||||
|
||||
### Revoking a Key
|
||||
|
||||
```http
|
||||
DELETE /api/v1/api-keys/{id}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SAML 2.0 SSO
|
||||
|
||||
Aegis supports SAML 2.0 for enterprise single sign-on (e.g. Azure AD, Okta, Ping).
|
||||
|
||||
### Configuration (admin only)
|
||||
|
||||
```http
|
||||
PUT /api/v1/sso/config
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"enabled": true,
|
||||
"idp_entity_id": "https://idp.example.com",
|
||||
"idp_sso_url": "https://idp.example.com/sso/saml",
|
||||
"idp_certificate": "-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
-----END CERTIFICATE-----",
|
||||
"sp_entity_id": "https://aegis.example.com",
|
||||
"attribute_mapping": {
|
||||
"email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
"role": "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SSO Login Flow
|
||||
|
||||
1. Browser visits `GET /api/v1/sso/login` → redirect to IdP
|
||||
2. User authenticates with IdP
|
||||
3. IdP POSTs SAML assertion to `POST /api/v1/sso/callback`
|
||||
4. Aegis validates assertion, finds/creates user, issues JWT cookie
|
||||
5. Browser redirected to frontend
|
||||
|
||||
### Service Provider Metadata
|
||||
|
||||
```http
|
||||
GET /api/v1/sso/metadata
|
||||
```
|
||||
|
||||
Returns XML metadata to register Aegis as a SAML service provider in your IdP.
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- **Login endpoint**: 5 attempts per minute per IP
|
||||
- Exceeded: `429 Too Many Requests`
|
||||
- Implemented via Redis counters with TTL
|
||||
|
||||
---
|
||||
|
||||
## SSRF Protection (Webhooks)
|
||||
|
||||
Webhook URLs are validated before saving. The following IP ranges are blocked:
|
||||
|
||||
| Range | Description |
|
||||
|-------|-------------|
|
||||
| 10.0.0.0/8 | Private (Class A) |
|
||||
| 172.16.0.0/12 | Private (Class B) |
|
||||
| 192.168.0.0/16 | Private (Class C) |
|
||||
| 169.254.0.0/16 | Link-local / APIPA |
|
||||
| 127.0.0.0/8 | Loopback |
|
||||
| ::1/128 | IPv6 loopback |
|
||||
| fc00::/7 | IPv6 unique local |
|
||||
| 0.0.0.0/8 | Reserved |
|
||||
| 100.64.0.0/10 | Shared address space |
|
||||
|
||||
Attempting to create a webhook pointing to a private IP returns:
|
||||
```json
|
||||
{"detail": "Webhook URL points to a private or reserved IP address (SSRF prevention)"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Audit Logging
|
||||
|
||||
Every significant action is recorded in the `audit_logs` table (admin read-only):
|
||||
|
||||
| Event type | Logged when |
|
||||
|------------|-------------|
|
||||
| `auth.login` | Successful login |
|
||||
| `auth.logout` | Logout |
|
||||
| `auth.login_failed` | Failed login attempt |
|
||||
| `auth.password_changed` | Password change |
|
||||
| `user.created` | New user created |
|
||||
| `user.updated` | User updated |
|
||||
| `user.deleted` | User deleted |
|
||||
| `test.created` | New test created |
|
||||
| `test.state_changed` | Test state transition |
|
||||
| `test.validated` | Both leads approved |
|
||||
| `campaign.completed` | Campaign completed |
|
||||
| `permission.denied` | 403 on a protected endpoint |
|
||||
|
||||
Query audit logs:
|
||||
```http
|
||||
GET /api/v1/audit-logs?user_id=<id>&event_type=auth.login&from=2024-01-01&to=2024-12-31
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user