Docs Auth Gate¶
Last Updated: August 10, 2026 Maintainer: Sho'Nuff Related: 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¶
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¶
- User visits
https://docs.itpropartner.com/ - Nginx makes an internal
auth_requesttohttp://127.0.0.1:8099/validate - Validator checks for
stack-accesscookie: - No cookie → returns 401 → nginx redirects to
/auth/sign-in - Cookie present → validates JWT:
- Decodes JWT without verification to read
emailclaim - Checks email against allowlist (hardcoded
DOCS_ALLOWEDset) - 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
- Decodes JWT without verification to read
- On 200, nginx serves the static docs content
Sign-In Flow¶
- User is redirected to
/auth/sign-in?redirect_to=https://docs.itpropartner.com/ - Validator serves an HTML sign-in form
- User enters email + password and submits
- Validator POSTs credentials to auth2 at
http://127.0.0.1:8102/api/v1/auth/password/sign-inusing the internal project publishable key - On success, auth2 returns an
access_token(JWT) - Validator sets
stack-accesscookie (HttpOnly, Secure, SameSite=Lax, domain=.itpropartner.com, Max-Age=24h) - Validator redirects browser to the original destination
Allowlist¶
Access is gated by email. The allowlist is in the validator's DOCS_ALLOWED set:
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:
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-inlocation explicitly disablesauth_requestto avoid infinite redirect loops
Troubleshooting¶
Check validator status¶
ssh app3 systemctl status docs-auth-validator
View validator logs¶
ssh app3 journalctl -u docs-auth-validator --since "10 min ago" --no-pager
Test authentication manually¶
# 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