Skip to Content

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 ClaimValue
user_idUser identifier
emailUser email address
display_nameHuman-readable name (may be null)
rolesArray of role strings
issaegis-auth
iatCurrent UTC timestamp
expCurrent 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 Handler

The Auth Middleware (internal/middleware/auth.go) performs the following:

  1. Skip list: Health checks (/health), token generation (/api/v1/auth/token), and public entity type endpoints bypass auth
  2. Read the JWT: Check the Authorization header for Bearer {token}, falling back to the aegis_token cookie. Decode using JWT_SECRET with HS256
  3. 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 PatternRequired Role
/api/v1/admin/*admin
/api/v1/detection-rules*power_user or admin
All other authenticated routesAny 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:

ParameterValue
Rate100 requests/minute (~1.67 req/sec)
Burst10 requests
Keyauthenticated user ID, falling back to remote IP
ImplementationGo 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.

EmailPasswordRoles
admin@aegis.localaegis-dev-adminadmin, 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

MethodPathDescription
POST /auth/tokenExchange email + password for JWTReturns {access_token, token_type, expires_in, user_id, roles, email, display_name}
POST /auth/validateValidate a JWT tokenUsed internally; returns {valid, user_id, roles}
* /auth/forward-authValidate the aegis_token cookie or Bearer headerUsed by Caddy; method-agnostic
GET /healthHealth checkReturns {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_SECRET default 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
Last updated on