Files
itpp-docs/docs-source/itpp-infrastructure/docs-auth-gate.md
T
Germaine Brown ba94b02fd9
Publish Docs Site / build (push) Failing after 6s
docs: add docs-auth-gate documentation — JWT validator, nginx config, sign-in flow, allowlist model
- 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
2026-08-10 16:43:18 -04:00

5.7 KiB

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

  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:

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-in location explicitly disables auth_request to 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