Auth Flow
AEGIS uses a two-step authentication flow: email/password credentials are exchanged for JWT tokens, and the JWT is validated by the API gateway on every request.
Flow Diagram
Browser Frontend API Gateway Auth Service
| :3000 :8000 :8009
| | | |
| 1. Enter email+password | | |
| ----------------------> | | |
| | | |
| 2. POST /api/v1/auth/token | |
| | -----------------> | -----------------> |
| | | {email,password} |
| | | |
| | | 3. Verify bcrypt |
| | | Lookup user |
| | | Sign JWT |
| | | <----------------- |
| | | {access_token, |
| | <----------------- | user_id, roles} |
| | | |
| 4. Store JWT in | | |
| httpOnly cookie | | |
| <---------------------- | | |
| | | |
| 5. API request | | |
| ----------------------> | | |
| | Authorization: | |
| | Bearer {token} | |
| | -----------------> | |
| | | |
| | 6. Decode JWT locally |
| | (HS256 + JWT_SECRET) |
| | Extract user_id, roles |
| | | |
| | 7. Add user context |
| | Forward to service |
| | | -------> Backend |
| | | |Step-by-Step
1. Login (Email/Password to JWT)
The frontend login page collects an email and password and exchanges them for a JWT:
Request:
POST /api/v1/auth/token
Content-Type: application/json
{"email": "admin@aegis.local", "password": "aegis-dev-admin"}Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 86400,
"user_id": "dev-user",
"roles": ["admin", "operator", "reviewer"],
"email": "admin@aegis.local",
"display_name": "Jane Operator"
}display_name may be null for accounts created without one; the frontend
header falls back to email, then user_id, for its human-readable label.
The auth service looks up the user by LOWER(email) in the users table, verifies the password against the stored bcrypt hash, then generates a JWT with:
| JWT Claim | Value |
|---|---|
user_id | User identifier |
email | User email address |
display_name | Human-readable name (may be null) |
roles | Array of role strings |
iss | aegis-auth |
iat | Current UTC timestamp |
exp | Current time + 24 hours |
The JWT is signed using HS256 with the JWT_SECRET environment variable.
2. Token Storage
The frontend stores the JWT in the aegis_token httpOnly cookie. The lib/api.ts centralized API client attaches the token to every subsequent request as an Authorization: Bearer {token} header, and the gateway also accepts the cookie directly for browser SSE/EventSource requests that cannot set headers.
3. Request Authentication
The API gateway validates every incoming request through its middleware chain:
CORS Middleware --> Rate Limiter --> Auth Middleware --> Route HandlerThe Auth Middleware (internal/middleware/auth.go) performs the following:
- Skip list: Health checks (
/health), token generation (/api/v1/auth/token), and public entity type endpoints bypass auth - Read the JWT: Check the
Authorizationheader forBearer {token}, falling back to theaegis_tokencookie. Decode usingJWT_SECRETwith HS256 - Reject: If the token is missing or invalid, return 401
On success, the middleware adds user_id and roles to the request context, available to downstream handlers.
4. Role-Based Access
Certain gateway routes enforce role requirements:
| Route Pattern | Required Role |
|---|---|
/api/v1/admin/* | admin |
/api/v1/detection-rules* | power_user or admin |
| All other authenticated routes | Any valid user |
The role check happens in the route handler, not in the auth middleware:
roles, _ := r.Context().Value(middleware.RolesKey).([]string)
if !hasRole(roles, "admin") {
http.Error(w, `{"error":"admin role required"}`, http.StatusForbidden)
return
}Rate Limiting
The gateway applies per-user rate limiting:
| Parameter | Value |
|---|---|
| Rate | 100 requests/minute (~1.67 req/sec) |
| Burst | 10 requests |
| Key | authenticated user ID, falling back to remote IP |
| Implementation | Go golang.org/x/time/rate token bucket |
When the limit is exceeded, the gateway returns:
HTTP 429 Too Many Requests
{"error": "rate limit exceeded -- 100 requests/minute"}Development Authentication
For local development, the auth service seeds a bootstrap admin on startup (from BOOTSTRAP_ADMIN_EMAIL / BOOTSTRAP_ADMIN_PASSWORD, defaulting to the values below). Additional users are created with the create_user CLI — there is no self-serve signup.
| Password | Roles | |
|---|---|---|
admin@aegis.local | aegis-dev-admin | admin, operator, reviewer |
The default bootstrap credentials grant all three roles. In production, set strong BOOTSTRAP_ADMIN_* values and provision real accounts via the CLI with granular role assignments.
Auth Service Endpoints
| Method | Path | Description |
|---|---|---|
POST /auth/token | Exchange email + password for JWT | Returns {access_token, token_type, expires_in, user_id, roles, email, display_name} |
POST /auth/validate | Validate a JWT token | Used internally; returns {valid, user_id, roles} |
* /auth/forward-auth | Validate the aegis_token cookie or Bearer header | Used by Caddy; method-agnostic |
GET /health | Health check | Returns {status: "ok", service: "auth-service"} |
Security Notes
- JWT tokens are signed with HS256 using the shared
JWT_SECRET. Both the auth service and the gateway use the same secret - The
JWT_SECRETdefault value (aegis-local-dev-secret-change-in-production) must be replaced for production deployments - JWT expiry is 24 hours. There is no refresh token mechanism in the current implementation
- Backend services do not re-validate JWTs — they trust that the gateway has already authenticated the request
- The gateway strips and re-applies CORS headers, ensuring backends cannot override the CORS policy