Summary
GET /saml/providers/{idp}/users is unreachable. Every request returns
401 {"error":"unauthorized","error_description":"Authentication required"}, including requests
carrying a valid bearer token that works on every other authenticated endpoint.
Found while triaging CATS run 20260730T220551Z: 69 of 69 requests to this path returned 401,
across HappyPath, ExtraHeaders, CheckSecurityHeaders, RandomResources,
AcceptLanguageHeaders and NewFields. There were no 200s and no 403s — the endpoint has never
worked.
Reproduction
$ TOK=<valid bearer token for charlie>
$ curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOK" http://rp2:30080/me
200
$ curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOK" http://rp2:30080/threat_models
200
$ curl -s -H "Authorization: Bearer $TOK" http://rp2:30080/saml/providers/tmi/users
{"error":"unauthorized","error_description":"Authentication required"}
Root cause
cmd/server/main.go listed "/saml/" in publicPathPrefixes, marking every /saml/* path
as not requiring authentication. PublicPathsMiddleware therefore set isPublicPath=true, the
JWT middleware skipped token validation, and userEmail / userID were never placed in the
request context.
ListSAMLUsers (api/saml_user_handlers.go:19) opens with GetAuthenticatedUser(c), which reads
exactly those context keys and returns 401 when they are absent. The handler is documented as
requiring auth because it exposes user emails, names and login times:
// SECURITY FIX: Validate authenticated user (prevents unauthorized access to user data)
// This endpoint exposes user emails, names, and login times - authentication is required
user, err := GetAuthenticatedUser(c)
So the endpoint failed closed — this is an availability defect, not a data exposure. The
handler's own check is what prevented the blanket public prefix from becoming a real hole.
Contract disagreement
The OpenAPI spec is unambiguous that only one /saml/* operation requires auth, and the
middleware contradicted it:
| operation |
x-public-endpoint |
security |
POST /saml/acs |
true |
[] |
GET /saml/slo |
true |
[] |
POST /saml/slo |
true |
[] |
GET /saml/providers |
true |
[] |
GET /saml/{provider}/login |
true |
[] |
GET /saml/{provider}/metadata |
true |
[] |
GET /saml/providers/{idp}/users |
absent |
[{"bearerAuth":[]}] |
Fix
Replace the blanket /saml/ prefix with the exact public paths (/saml/acs, /saml/slo,
/saml/providers) plus an isPublicSAMLPath helper for the two provider-scoped routes
(/saml/{provider}/login, /saml/{provider}/metadata), which have a variable segment and so
cannot be expressed as a prefix. Matching exactly three segments means any future /saml/ route
stays private until it is added deliberately, instead of silently inheriting public status.
Notes
The latent fragility is the more important half: with the blanket prefix, any authenticated
endpoint added under /saml/ would silently lose its JWT context. Whether it then fails closed
(401, like this one) or leaks depends entirely on whether the individual handler happens to
re-check auth.
Fixing this also removes 19 true positives from the CATS baseline, which had been misattributed
to "SAML not configured in dev".
Summary
GET /saml/providers/{idp}/usersis unreachable. Every request returns401 {"error":"unauthorized","error_description":"Authentication required"}, including requestscarrying a valid bearer token that works on every other authenticated endpoint.
Found while triaging CATS run
20260730T220551Z: 69 of 69 requests to this path returned 401,across
HappyPath,ExtraHeaders,CheckSecurityHeaders,RandomResources,AcceptLanguageHeadersandNewFields. There were no 200s and no 403s — the endpoint has neverworked.
Reproduction
Root cause
cmd/server/main.golisted"/saml/"inpublicPathPrefixes, marking every/saml/*pathas not requiring authentication.
PublicPathsMiddlewaretherefore setisPublicPath=true, theJWT middleware skipped token validation, and
userEmail/userIDwere never placed in therequest context.
ListSAMLUsers(api/saml_user_handlers.go:19) opens withGetAuthenticatedUser(c), which readsexactly those context keys and returns 401 when they are absent. The handler is documented as
requiring auth because it exposes user emails, names and login times:
So the endpoint failed closed — this is an availability defect, not a data exposure. The
handler's own check is what prevented the blanket public prefix from becoming a real hole.
Contract disagreement
The OpenAPI spec is unambiguous that only one
/saml/*operation requires auth, and themiddleware contradicted it:
x-public-endpointsecurityPOST /saml/acs[]GET /saml/slo[]POST /saml/slo[]GET /saml/providers[]GET /saml/{provider}/login[]GET /saml/{provider}/metadata[]GET /saml/providers/{idp}/users[{"bearerAuth":[]}]Fix
Replace the blanket
/saml/prefix with the exact public paths (/saml/acs,/saml/slo,/saml/providers) plus anisPublicSAMLPathhelper for the two provider-scoped routes(
/saml/{provider}/login,/saml/{provider}/metadata), which have a variable segment and socannot be expressed as a prefix. Matching exactly three segments means any future
/saml/routestays private until it is added deliberately, instead of silently inheriting public status.
Notes
The latent fragility is the more important half: with the blanket prefix, any authenticated
endpoint added under
/saml/would silently lose its JWT context. Whether it then fails closed(401, like this one) or leaks depends entirely on whether the individual handler happens to
re-check auth.
Fixing this also removes 19 true positives from the CATS baseline, which had been misattributed
to "SAML not configured in dev".