# 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`