Session Log: OAuth2 authenticated path completed end-to-end; ADR 014 visibility verified; reverse proxy fixed
Date: 2026-08-20 (second session of the day; continues
2026-08-20-checkauthstatus-forces-logout-root-caused.md)
Participants: ys2n, Claude Code
Outcome: ADR 014's authenticated Solr proxy path now works end-to-end and is proven
correct, not merely functional. Five PRs merged (#128–#132). The 4th and last OAuth2
defect is fixed and verified live; visibility filtering is verified with a positive and
negative discriminator; the http:// RelayState bug is fixed. Two regression tests and two
how-tos are now in the repo.
⚠ Hand-written, not machine-generated. Verifying /oauth/userinfo necessarily returns
the authenticated user's OIDC profile claims, and the test identity is authmapped to a real
account — so the raw transcript contains a real name and email. save-session-log.py was
deliberately not run. (Yuji assessed the PII itself as not a concern; the log is still
hand-written so the transcript is not republished verbatim into a public repo.)
What shipped
| PR | |
|---|---|
| #128 | mandala_saml_oauth — exempts OAuth2 Bearer requests from checkAuthStatus() |
| #129 | Live verification of that fix |
| #130 | scripts/verify-oauth2-userinfo.{py,sh} + how-to |
| #131 | reverse_proxy / trusted_host_patterns in settings.php |
| #132 | scripts/verify-solr-proxy-visibility.py + how-to |
main clean, no open PRs.
The headline result
The full chain — SAML login → OAuth2 authorization_code → Bearer /oauth/userinfo →
proxy query with the Redis visibility token — works, and the right people see the right
things:
| anonymous | uid 600 | |
|---|---|---|
images-11-95599 (private, their collection) |
hidden | visible |
images-11-3 (private, someone else's) |
hidden | hidden |
1631777 (public) |
visible | visible |
*:* |
751,032 | 751,566 |
Client fq injection is stripped; a forged sid falls back to anonymous.
Three false alarms, all mine, all now documented
This is the part worth carrying forward. Each looked like a product defect and was a testing or reasoning error:
- The designed fix was a no-op. The previous session's fix tested
$this->account instanceof TokenAuthUserInterface.@current_useris anAccountProxythat wraps the real account, so it never matches. Caught by reading core before shipping. Detection now usesSimpleOauthRequestPolicyInterface::isOauth2Request(). - A post-deploy test raced the deploy. The watcher gated on the module directory
appearing;
deploy_backend.ymlstarts the container ~50s beforeimport full site configurationenables modules, and aServiceProvideronly runs for an enabled module. The replay landed 12 seconds inside that window and reported a confident FAIL on a working fix. idis not unique in kmassets. Four documents share id1821— the places/subjects/terms taxonomy shadows (ADR 006) plus an audio-video asset. Using it as a fixture producedgot 4, expected 0, which reads exactly like a visibility breach. The four visible documents were the public shadows; the private asset was correctly withheld.
A fourth correction, from the earlier session: the watchdog evidence blamed on
checkAuthStatus() for Xiaoming's logout report was our own test traffic — every
Session closed belonged to uid 600 inside our own testing windows.
Reverse proxy — what the investigation turned up
reverse_proxy/trusted_host_patterns were never configured, so Request::getUri()
returned http:// and the SAML RelayState was built with the wrong scheme. Two findings
beyond the obvious fix:
- Each VPC carries two CIDR blocks, not one (staging
10.130.109.0/24+10.130.112.0/24; production10.130.110.0/24+10.130.113.0/24), and health checks arrive from both. Listing only the primaries half-works — Drupal falls back silently rather than erroring, so the scheme would be wrong intermittently with no log line. trusted_host_patternsfails closed: an unmatched Host is a 400, and the health check on/expects 200–299, so a wrong pattern set takes targets unhealthy. Patterns were tested against 27 real hostnames and 5 hostile ones before deploying. The exact health-check Host could not be captured (ALB access logs exclude health checks; no packet-capture tooling on dev-0), so the IPv4 pattern is deliberately broad and the code comment says so rather than asserting AWS behaviour as fact.
Verified after deploy: 8/8 host-header checks, RelayState now https://, both ALB
targets stayed healthy, and the OAuth2 regression test still passes.
Still open
- NetBadge logout — the IdP advertises no
SingleLogoutServiceandlogout_goto_urlis unset, so a Drupal logout never ends the NetBadge session. Needs the canonical logout URL from UVA ITS and atrusted.url.domainsentry (absent entirely today). The note carries a classification flag for Yuji. - 8h vs 23d session expiry — upstream MR !48 is RTBC against our
4.xbranch and fixes where the user lands; whether 8h is the right lifetime is undecided. - Cron re-run loop on dev-0 (26 entries/48h) — cause still uninvestigated.
- A one-off
TypeError: Cannot assign null to property TokenAuthUser::$consumer.
Related
docs/deferred/simplesamlphp-checkauthstatus-forces-logout-oauth-and-maybe-browser.mddocs/deferred/saml-logout-does-not-terminate-netbadge-idp-session.mddocs/deferred/saml-session-expires-8h-forcing-logout-mid-session.mddocs/dev-notes/howto-verify-oauth2-authenticated-path.mddocs/dev-notes/howto-verify-solr-proxy-visibility.md