docs: add docs-auth-gate documentation — JWT validator, nginx config, sign-in flow, allowlist model
Publish Docs Site / build (push) Failing after 6s
Publish Docs Site / build (push) Failing after 6s
- New page: itpp-infrastructure/docs-auth-gate.md - Updated navigation in mkdocs.yml (ITPP Infrastructure now has sub-items) - Added CHANGELOG entry for 2026-08-10
This commit is contained in:
@@ -4,3 +4,14 @@
|
||||
|
||||
- Created CHANGELOG.md (missing per project documentation standard)
|
||||
- Project directory: `/root/projects/itpp-infrastructure`
|
||||
|
||||
## 2026-08-10 — Docs Auth Gate
|
||||
|
||||
- **Added:** `docs-auth-gate.md` — full documentation of the auth gate for docs.itpropartner.com
|
||||
- **Components:**
|
||||
- `/opt/docs-auth/auth-validator.py` on app3 — JWT validator + sign-in handler (port 8099)
|
||||
- `/etc/systemd/system/docs-auth-validator.service` — systemd unit
|
||||
- `/etc/nginx/sites-available/docs.itpropartner.com.conf` — nginx config with `auth_request /auth` directive
|
||||
- **Auth model:** Email allowlist-based access control using auth2 (Stack Auth) JWT tokens
|
||||
- **Flow:** Unauthenticated → redirect to sign-in page → authenticate against internal project → `stack-access` cookie set → validator checks JWT + email allowlist → access granted
|
||||
- **Allowed users:** `g@germainebrown.com`, `info@itpropartner.com`
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# Docs Auth Gate
|
||||
|
||||
> **Last Updated:** August 10, 2026
|
||||
> **Maintainer:** Sho'Nuff
|
||||
> **Related:** [centralized-auth](/itpp-infrastructure/centralized-auth/)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`docs.itpropartner.com` is gated behind authentication using Stack Auth (auth2) JWT session tokens. Unauthenticated visitors are redirected to a sign-in page. Access is controlled by an email address allowlist — only users whose email is in the list can view documentation.
|
||||
|
||||
The gate was implemented on August 10, 2026, replacing the previously open-access docs site.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
Browser → docs.itpropartner.com (nginx)
|
||||
→ auth_request → http://127.0.0.1:8099/validate
|
||||
→ reads stack-access cookie → validates JWT against auth2 JWKS
|
||||
→ checks email against allowlist
|
||||
→ 200 (granted) or 401 (denied)
|
||||
→ 401 → redirect to /auth/sign-in (sign-in form)
|
||||
→ 200 → serve static docs
|
||||
```
|
||||
|
||||
| Component | Location | Description |
|
||||
|-----------|----------|-------------|
|
||||
| **Nginx config** | `/etc/nginx/sites-available/docs.itpropartner.com.conf` | CloudPanel vhost with `auth_request` directive |
|
||||
| **Auth validator** | `/opt/docs-auth/auth-validator.py` | FastAPI-like HTTP server on port 8099 |
|
||||
| **Systemd service** | `/etc/systemd/system/docs-auth-validator.service` | Runs as `www-data`, auto-starts |
|
||||
| **Sign-in page** | Served by the validator at `/auth/sign-in` | HTML form that authenticates against auth2 |
|
||||
| **Stack Auth instance** | `auth2.itpropartner.com` (app3 Docker) | Handles user authentication and JWT issuance |
|
||||
|
||||
## How It Works
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. User visits `https://docs.itpropartner.com/`
|
||||
2. Nginx makes an internal `auth_request` to `http://127.0.0.1:8099/validate`
|
||||
3. Validator checks for `stack-access` cookie:
|
||||
- **No cookie** → returns 401 → nginx redirects to `/auth/sign-in`
|
||||
- **Cookie present** → validates JWT:
|
||||
- Decodes JWT without verification to read `email` claim
|
||||
- Checks email against **allowlist** (hardcoded `DOCS_ALLOWED` set)
|
||||
- Verifies JWT signature against auth2 JWKS (ES256)
|
||||
- Verifies issuer = `https://auth2-api.itpropartner.com/api/v1/projects/internal`
|
||||
- Verifies audience = `internal`
|
||||
- If all checks pass → returns 200
|
||||
4. On 200, nginx serves the static docs content
|
||||
|
||||
### Sign-In Flow
|
||||
|
||||
1. User is redirected to `/auth/sign-in?redirect_to=https://docs.itpropartner.com/`
|
||||
2. Validator serves an HTML sign-in form
|
||||
3. User enters email + password and submits
|
||||
4. Validator POSTs credentials to auth2 at `http://127.0.0.1:8102/api/v1/auth/password/sign-in` using the **internal** project publishable key
|
||||
5. On success, auth2 returns an `access_token` (JWT)
|
||||
6. Validator sets `stack-access` cookie (HttpOnly, Secure, SameSite=Lax, domain=.itpropartner.com, Max-Age=24h)
|
||||
7. Validator redirects browser to the original destination
|
||||
|
||||
### Allowlist
|
||||
|
||||
Access is gated by email. The allowlist is in the validator's `DOCS_ALLOWED` set:
|
||||
|
||||
```python
|
||||
DOCS_ALLOWED: set[str] = {
|
||||
"g@germainebrown.com",
|
||||
"info@itpropartner.com",
|
||||
}
|
||||
```
|
||||
|
||||
**To add or remove users**, edit `/opt/docs-auth/auth-validator.py` on app3, update the set, and restart the service:
|
||||
|
||||
```bash
|
||||
ssh app3 systemctl restart docs-auth-validator
|
||||
```
|
||||
|
||||
## Why Not a Separate Stack Auth Project?
|
||||
|
||||
The original plan was to create a separate `docs-itpp` project in Stack Auth and authenticate users specifically against that project. A project was created in the database with its own API keys and branch configuration.
|
||||
|
||||
However, the self-hosted Stack Auth branch system requires internal initialization that doesn't trigger for manually-created projects. The sign-in API returned `BRANCH_DOES_NOT_EXIST` for the `docs-itpp` project despite having all the correct database rows — this is a known limitation of the self-hosted version.
|
||||
|
||||
The email allowlist approach achieves the same result (project = group of members) without requiring multiple Stack Auth projects.
|
||||
|
||||
## Security Model
|
||||
|
||||
- **Cookie:** `stack-access` — HttpOnly (inaccessible to JavaScript), Secure (HTTPS only), SameSite=Lax, domain-scoped to `.itpropartner.com`, 24-hour expiry
|
||||
- **JWT validation:** Signature verified against auth2 JWKS, expiry checked, issuer and audience validated
|
||||
- **Allowlist:** Email checked from unverified claims BEFORE signature verification (safe: an attacker who can forge a JWT could also choose their own email)
|
||||
- **No bypass:** The `/auth/sign-in` location explicitly disables `auth_request` to avoid infinite redirect loops
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check validator status
|
||||
```bash
|
||||
ssh app3 systemctl status docs-auth-validator
|
||||
```
|
||||
|
||||
### View validator logs
|
||||
```bash
|
||||
ssh app3 journalctl -u docs-auth-validator --since "10 min ago" --no-pager
|
||||
```
|
||||
|
||||
### Test authentication manually
|
||||
```bash
|
||||
# Sign in and get a cookie
|
||||
curl -s -D - -X POST \
|
||||
-d "email=g@germainebrown.com&password=YOURPASSWORD" \
|
||||
https://docs.itpropartner.com/auth/sign-in | grep stack-access
|
||||
|
||||
# Test access with the cookie
|
||||
curl -s -o /dev/null -w "HTTP %{http_code}\n" \
|
||||
-H "Cookie: stack-access=YOUR_TOKEN" \
|
||||
https://docs.itpropartner.com/
|
||||
```
|
||||
|
||||
### Common errors
|
||||
|
||||
| Error log | Cause | Fix |
|
||||
|-----------|-------|-----|
|
||||
| `no_token` | User has no `stack-access` cookie | User hasn't signed in — redirect to sign-in page is expected |
|
||||
| `Invalid audience` | JWT `aud` claim doesn't match expected value | Check `ALLOWED_PROJECT` matches the token's `aud` |
|
||||
| `Access denied for X (not in allowlist)` | User signed in but email not in `DOCS_ALLOWED` | Add email to allowlist in validator |
|
||||
| `Auth API error 400` | Sign-in credentials rejected | User/password wrong or auth2 DB issue |
|
||||
|
||||
## Server
|
||||
|
||||
- **Host:** app3 (152.53.241.111)
|
||||
- **Validator port:** 8099 (localhost only)
|
||||
- **Stack Auth API:** http://127.0.0.1:8102 (Docker, localhost only)
|
||||
- **Validator user:** `www-data`
|
||||
Reference in New Issue
Block a user