Document real Auth0 SAML verification, closing §3b -- the last SSO gap

Auth0's SAML2 Web App addon (the same dev tenant §3a used) stood in as
a real SAML IdP, over a genuine self-signed TLS proxy in front of
enterprise-auth (required, not optional, for SAML's SameSite=None
cookie). Full round trip confirmed: real signed assertion, audience/
destination/signature validation, correct multi-membership handling,
and POST /internal/authorize returning the selected tenant/role.
Updates the runbook's verification status, §3b, and the threat model's
"Read this first" finding and summary table to reflect this and the
isSecureRequest fix it found. §7/§11's live-cluster steps (no
kind/kubectl in this environment) are now the only remaining gap in the
entire runbook.
This commit is contained in:
2026-08-15 18:08:25 -07:00
parent f5ca09f686
commit 783d66786b
2 changed files with 96 additions and 56 deletions
+82 -42
View File
@@ -60,29 +60,42 @@ Docker-free testing could have caught:
`/docs/security/threat-model.md`'s "Read this first" section for the
full account.
**What's still not verified, and why**: §3a (OIDC) and §12 (tenant
picker, both single- and multi-membership paths) are now closed --
verified against a real Auth0 developer tenant, full browser round
trips, real session cookies authorizing correctly, including selecting
between two real tenant memberships and getting back the right
tenant/role each time (see §3a and §12 below). That pass also found and
fixed a real bug: `web/Dockerfile` only declared `ARG`/`ENV` for
`VITE_API_BASE_URL`, so `docker-compose.yml`'s build args for
`VITE_ALERTING_API_BASE_URL`/`VITE_ENTERPRISE_AUTH_BASE_URL` were
silently dropped by Docker (an undeclared `--build-arg` is dropped, not
an error) -- `enterpriseAuthBase` came out `undefined` in the built
bundle, so the tenant-picker page threw "enterprise-auth is not
configured" against a real running container even though
`docker-compose.yml` looked correct. Fixed by declaring all three.
**What's still not verified, and why**: §3a (OIDC), §3b (SAML), and §12
(tenant picker, both single- and multi-membership paths) are now all
closed -- verified against a real Auth0 developer tenant acting as both
an OIDC and a SAML IdP, full browser round trips, real session cookies
authorizing correctly, including selecting between two real tenant
memberships and getting back the right tenant/role each time (see §3a,
§3b, and §12 below). That pass found and fixed two real bugs:
§3b (SAML) still needs a real external IdP with SAML app support, which
this environment doesn't have credentials for -- the fake-IdP test (real
XML signing/verification, no stand-in shortcuts) is the strongest
evidence available without one. §7 and §11's live-cluster steps need
`kind`/`kubectl`, which aren't installed in this environment -- their
offline-only checks (`go build`/`go vet`/`go test`, `helm lint`, `helm
template` + parsing the rendered YAML) all pass and are documented as
such below.
1. `web/Dockerfile` only declared `ARG`/`ENV` for `VITE_API_BASE_URL`,
so `docker-compose.yml`'s build args for
`VITE_ALERTING_API_BASE_URL`/`VITE_ENTERPRISE_AUTH_BASE_URL` were
silently dropped by Docker (an undeclared `--build-arg` is dropped,
not an error) -- `enterpriseAuthBase` came out `undefined` in the
built bundle, so the tenant-picker page threw "enterprise-auth is not
configured" against a real running container even though
`docker-compose.yml` looked correct.
2. `enterprise/internal/loginhandler.go` decided every cookie's `Secure`
attribute from `r.TLS != nil` alone -- wrong for the deployment shape
this handler actually runs in, since `enterprise-auth` never
terminates TLS itself. Closing SAML's real-IdP gap required a genuine
TLS-terminating proxy in front of it (SAML's request cookie is
`SameSite=None`, which the cookie spec requires to be paired with
`Secure`, and Chrome silently drops it otherwise), which is exactly
what surfaced this: `r.TLS` was nil at the Go process even over a
real HTTPS client connection, so the cookie came back non-`Secure`
and Chrome dropped it, breaking the flow. Fixed with an
`isSecureRequest(r)` helper that also honors
`X-Forwarded-Proto: https` -- affects every cookie this handler sets,
not just SAML's, so this would have hit any real reverse-proxied
deployment, not just this test.
§7 and §11's live-cluster steps still need `kind`/`kubectl`, which
aren't installed in this environment -- their offline-only checks
(`go build`/`go vet`/`go test`, `helm lint`, `helm template` + parsing
the rendered YAML) all pass and are documented as such below. That's the
only remaining gap in this entire runbook that isn't already closed.
If you're reading this to decide whether Phase 4 is production-ready:
closer than before, but not yet -- see
@@ -210,44 +223,71 @@ the one thing here still only reachable via the HTTP endpoints
directly, no operator flag, since `sentryctl dashboards permissions
list|grant|revoke` already exists as that surface instead (§5a).
## 3b. `enterprise-auth`: human login via SAML (new -- same "verified
live in this session, not against a real running container or a real
external IdP" caveat as §3a)
## 3b. `enterprise-auth`: human login via SAML (now genuinely verified
against a real external IdP, over real HTTPS)
`enterprise/internal/loginhandler`'s SAML tests already prove the
mechanism works end to end against a real fake SAML IdP (`go test
./internal/loginhandler/... -run SAML -v` from `enterprise/`, no Docker
needed). What's still unverified is wiring it into this actual running
stack. To try that for real, point `docker-compose.yml`'s
`enterprise-auth` service at a real SAML IdP (many identity providers
needed). **Closed for real in this pass**, using the same Auth0
developer tenant §3a used, via that app's **SAML2 Web App addon**
(Auth0 acting as a real SAML IdP, not just OIDC). This is where SAML's
`SameSite=None` request cookie (which the cookie spec requires to be
paired with `Secure`) made the plain-HTTP setup this environment
otherwise defaults to a hard blocker -- getting a real assertion back
required standing up a genuine (self-signed, dev-only) TLS-terminating
nginx proxy in front of `enterprise-auth`. Doing that surfaced a real,
previously-undiscovered bug: every cookie `loginhandler.go` sets decided
`Secure` from `r.TLS != nil` alone, which is wrong for the deployment
shape this handler actually runs in -- `enterprise-auth` never
terminates TLS itself, so in any real deployment (behind an ingress/load
balancer, exactly what the proxy here stands in for) `r.TLS` is nil even
over a genuinely HTTPS client connection. The `Secure` attribute was
silently missing behind the proxy, and Chrome dropped the cookie outright
-- fixed with a new `isSecureRequest(r)` helper that also honors
`X-Forwarded-Proto: https`, with its own regression test
(`TestHandleSAMLLoginSetsSecureCookieBehindATLSProxy`). This bug affects
every cookie this handler sets, not just SAML's, and would have hit
production the same way, so this generalizes well beyond closing this
one section.
With that fixed, the full flow completed for real: login redirected to
Auth0's real SAML SSO endpoint, Auth0 posted back a real signed
assertion, `enterprise/internal/saml` validated it (audience, destination,
signature) and extracted the email attribute, and the identity landed on
the real `/select-tenant` page with both real tenant memberships,
exactly like §12's OIDC walkthrough -- confirmed via
`POST /internal/authorize` returning the selected tenant/role.
To try this yourself, point `docker-compose.yml`'s `enterprise-auth`
service at a real SAML IdP (many identity providers, including Auth0,
offer a free developer/trial tenant with SAML app support):
```sh
# Add to enterprise-auth's environment in docker-compose.yml (or a
# docker-compose.override.yml):
# SAML_ENTITY_ID: "http://localhost:8082/saml/metadata"
# SAML_ACS_URL: "http://localhost:8082/auth/saml/acs"
# SAML_ENTITY_ID: "https://<your-https-host>/saml/metadata"
# SAML_ACS_URL: "https://<your-https-host>/auth/saml/acs"
# SAML_IDP_METADATA_URL: "https://your-idp.example.com/metadata"
# Register SAML_ENTITY_ID/SAML_ACS_URL with the IdP's application config
# -- the IdP needs Sentry's ACS URL to know where to POST the assertion.
# Register SAML_ENTITY_ID (as "audience")/SAML_ACS_URL (as the
# "Application Callback URL") with the IdP's application config -- the
# IdP needs Sentry's ACS URL to know where to POST the assertion, and
# the two must actually agree or the SP-side validation fails with a
# generic "Authentication failed" (found the hard way -- crewjam/saml's
# error here doesn't distinguish audience mismatch from other causes).
# <your-https-host> must be real HTTPS, not plain HTTP -- see above.
docker compose up -d --build enterprise-auth
curl -s http://localhost:8082/auth/features
curl -s https://<your-https-host>/auth/features
# expect: {"sso_configured":true,"oidc_enabled":false,"saml_enabled":true}
```
Bootstrapping the first `tenant_memberships` row uses the same
`-create-tenant`/`-grant-membership-*` flags as §3a (log in once, it
fails with 403, grant the membership using the email you logged in
with, log in again). Then visit
`http://localhost:8082/auth/saml/login` in a real browser, complete the
IdP's login, and confirm a `sentry_session` cookie lands after redirect
to `POST_LOGIN_REDIRECT_URL`. Note SAML's `sentry_saml_request` cookie
is `SameSite=None`, which requires `Secure` -- i.e. this only works over
HTTPS in a real deployment, unlike OIDC's redirect-based callback which
tolerates plain HTTP for local dev (see
`enterprise/internal/loginhandler/loginhandler.go`'s `handleSAMLLogin`
doc comment for why).
with, log in again). Then visit `/auth/saml/login` on your HTTPS host in
a real browser, complete the IdP's login, and confirm a `sentry_session`
cookie lands after redirect to `POST_LOGIN_REDIRECT_URL`.
## 4. Turn on RBAC enforcement and prove it actually blocks/allows