You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-**Password hash** is tagged `json:"-"` on the User model — never exposed in API responses.
61
61
-**Email enumeration** is prevented: login returns the same error for wrong email and wrong password.
62
+
-**Base domain restriction:** When `AEGIS_BASE_DOMAIN` is set, all public auth endpoints (register, login, logout, forgot-password, reset-password, MFA validate, verify-email) are blocked on org subdomains via `baseOnlyMiddleware`. Auth flows must happen on the base domain only. Cookies are set with `Domain=.baseDomain` for cross-subdomain sharing. The UI redirects users from subdomain auth pages to the base domain with a `?return_to=` param.
62
63
63
64
### Agent Authentication
64
65
@@ -143,6 +144,13 @@ Each org tracks a `schema_version` in `common.organizations`. This allows:
143
144
-**Context:** Use `middleware.UserFromContext(ctx)` and `middleware.OrgFromContext(ctx)` to access the authenticated user and current org.
144
145
-**Tenant store:** Use `tenantStore(r)` helper in handlers (calls `middleware.TenantStoreFromContext`).
145
146
-**Imports:** Group as stdlib → external → internal.
147
+
-**Logging:** Use `log/slog` (Go stdlib). Never use `fmt.Printf`, `log.Printf`, or `log.Println` for application logging.
148
+
-`slog.Info("message", "key", value)` — normal operational events
149
+
-`slog.Warn("message", "key", value)` — degraded but recoverable situations
150
+
-`slog.Error("message", "error", err)` — failures that need attention
4.**Agent** — Bearer token (org resolved from subdomain/header, no membership check): `/api/v1/agent/*`
225
+
1.**Infrastructure (any domain, no auth)** — registered on the top-level mux outside the API server, bypasses all middleware. Accessible on base domain, org subdomains, custom domains, IPs — anywhere. Used by load balancers, Kubernetes probes, and monitoring systems: `/healthz`, `/readyz`, `/metrics`
226
+
2.**Public (base domain only)** — no auth required, but blocked on org subdomains when `AEGIS_BASE_DOMAIN` is set: `/api/v1/auth/register`, `/api/v1/auth/login`, `/api/v1/auth/logout`, `/api/v1/auth/forgot-password`, `/api/v1/auth/reset-password`, `/api/v1/auth/mfa/validate`, `/api/v1/auth/verify-email`
227
+
3.**Public (any domain)** — no auth, works everywhere: `/api/v1/config/auth`, `/api/v1/docs`, `/api/v1/docs/openapi.yaml`
Copy file name to clipboardExpand all lines: README.md
+48-2Lines changed: 48 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,6 +47,43 @@ npm run dev
47
47
# Open http://localhost:5173
48
48
```
49
49
50
+
### Subdomain Testing (local)
51
+
52
+
Aegis supports subdomain-based org resolution (e.g., `acme.aegis.io`). To test this locally, we use [`lvh.me`](http://lvh.me) — a public domain where `*.lvh.me` resolves to `127.0.0.1`. No `/etc/hosts` changes needed.
53
+
54
+
**Setup:**
55
+
56
+
```bash
57
+
# .env (already set by default in .env.example)
58
+
AEGIS_BASE_DOMAIN=lvh.me
59
+
60
+
# Restart
61
+
docker compose up --build -d
62
+
```
63
+
64
+
**Usage:**
65
+
66
+
| URL | What happens |
67
+
|---|---|
68
+
|`http://lvh.me:8080`| Base domain — no subdomain, header-only mode |
69
+
|`http://test.lvh.me:8080`| Resolves org with slug `test` from subdomain |
70
+
|`http://acme.lvh.me:8080`| Resolves org with slug `acme` from subdomain |
71
+
| Any new org slug | Works instantly — `*.lvh.me` is a wildcard |
@@ -130,9 +132,32 @@ This is validated by regex `^org_[a-f0-9]{32}$` to prevent SQL injection.
130
132
131
133
## Auth Flow
132
134
135
+
### Base Domain Restriction
136
+
137
+
When `AEGIS_BASE_DOMAIN` is set (e.g., `aegis.io`), all auth flows are restricted to the base domain only. Requests to org subdomains (e.g., `acme.aegis.io/api/v1/auth/login`) are rejected with a 403 error.
138
+
139
+
This applies to: register, login, logout, forgot-password, reset-password, MFA validate, MFA send-email-otp, and verify-email endpoints.
140
+
141
+
**Defense in depth:**
142
+
-**Server-side:**`baseOnlyMiddleware` rejects auth API calls from any host that isn't the exact base domain (subdomains, IPs, unknown hostnames are all blocked)
143
+
-**UI-side:**`useSubdomainAuthRedirect` hook redirects auth pages to the base domain
144
+
-**Cookie scoping:** Auth cookies are set with `Domain=.aegis.io` so they work across all subdomains
145
+
146
+
**Login redirect flow:**
147
+
```
148
+
User visits acme.aegis.io (no session)
149
+
→ UI redirects to aegis.io/login?return_to=https://acme.aegis.io/
150
+
→ User logs in on aegis.io (cookie set with Domain=.aegis.io)
151
+
→ UI redirects back to acme.aegis.io/ (cookie is valid on subdomain)
152
+
```
153
+
154
+
The `return_to` parameter is validated to prevent open redirect attacks — only URLs sharing the same base domain are allowed.
155
+
156
+
**Exceptions:**`GET /api/v1/auth/me` works on any subdomain (it's authenticated, not public, and the UI needs it to check session status).
157
+
133
158
### Registration
134
159
```
135
-
POST /api/v1/auth/register
160
+
POST /api/v1/auth/register (base domain only when AEGIS_BASE_DOMAIN set)
136
161
│
137
162
├─ Check feature flag: signup enabled?
138
163
├─ Validate email, password (8+ chars, upper+lower+digit), name
@@ -142,22 +167,23 @@ POST /api/v1/auth/register
142
167
├─ Create default org ("Name's Org")
143
168
├─ Add user as org owner
144
169
├─ Generate JWT (24h TTL)
145
-
└─ Set aegis_token HttpOnly cookie
170
+
└─ Set aegis_token HttpOnly cookie (Domain=.baseDomain when set)
146
171
```
147
172
148
173
### Login
149
174
```
150
-
POST /api/v1/auth/login
175
+
POST /api/v1/auth/login (base domain only when AEGIS_BASE_DOMAIN set)
151
176
│
152
177
├─ Find user by email
153
178
├─ Compare bcrypt hash (same error for wrong email/password)
179
+
├─ If MFA enabled: return mfa_required + short-lived MFA token
154
180
├─ Generate JWT
155
-
└─ Set aegis_token HttpOnly cookie
181
+
└─ Set aegis_token HttpOnly cookie (Domain=.baseDomain when set)
156
182
```
157
183
158
184
### Session Check
159
185
```
160
-
GET /api/v1/auth/me
186
+
GET /api/v1/auth/me (works on any subdomain)
161
187
│
162
188
├─ Auth middleware validates cookie
163
189
├─ Load user + their orgs
@@ -190,5 +216,7 @@ Each org's data is fully isolated at the database level:
0 commit comments