Part of #589.
CONTEXT
hydraorganization has no web routes and no user identity; auth is one static admin bearer token (internal/api/server.go:49-53). This issue adds iamnim login and per-request org authorization, using the one-time code handoff so the session token never appears in a URL. The ?token= pattern is NOT acceptable here; see the master issue's accepted-risk rule 6.
SCOPE
ACCEPTANCE CRITERIA
FILES
DEPLOY
Tag, push, CI publishes to releases.experiencenet.com, systemd host auto-updates within 6h (or run hydraorganization update). Verify with hydrarelease verify --project hydraorganization.
CODE IMPLEMENTED in hydraorganization v0.6.0, currently OFF in production.
Setting iamnim.base_url turns it on. /dashboard requires a session and redirects to /dashboard/login, which sends the person to iamnim. /dashboard/authed catches the returning session, moves it into an HttpOnly Secure SameSite=Lax cookie and redirects in ONE hop with Referrer-Policy: no-referrer, so the token does not stay in the address bar, in history, or in a Referer; the query string is never logged. The one-time code exchange that removes the query parameter entirely is #591 and remains the real fix.
Org scoping: a person sees exactly the orgs they are a member of. ?org= is validated against membership and never trusted; more than one membership renders a switcher; zero renders a plain no-access page that deliberately names no organization. GET /api/v1/organizations/{id}/overview answers 401 rather than redirecting a machine client. The dashboard provider is now keyed by org with a per-org cache instead of one configured org. Tests cover: non-member org rejected, default org selection, unauthenticated redirect, JSON 401, and that the authed handler sets an HttpOnly+Secure cookie and strips the token from the redirect.
WHY IT IS OFF: enabling it without a Pantheon membership locks out the intended reader, and no membership exists yet (#590). Verified against live iamnim that a junk session is correctly rejected and the login redirect is well formed.
TO ENABLE, in order: (1) the owner signs into iamnim once so their user record exists in realm r_349832f91720; (2) create the Pantheon org with slug matching the hydraorganization org id (visit-flanders); (3) grant the membership; (4) add iamnim.base_url to /srv/scales/hydraorganization/config.yaml and restart the scale. I could not do step 2 or 3: Pantheon stores only a hash of its admin key, so the plaintext is not recoverable from the host.
ENABLED IN PRODUCTION 2026-09-01, now that #590 granted the membership. iamnim.base_url is set in /srv/scales/hydraorganization/config.yaml and the scale restarted on hydraorganization v0.6.1.
Verified live: GET /dashboard with no session returns 303 to /dashboard/login; /dashboard/login returns 303 to https://iamnim.com/login with redirect_uri pointing at /dashboard/authed; GET /api/v1/organizations/visit-flanders/overview with no session returns 401 with a JSON error rather than a redirect. The dashboard is no longer publicly readable, which closes the exposure raised when #597 shipped live data.
Remaining on this issue: the session still arrives from iamnim as a ?token= query parameter. It is scrubbed in one hop into an HttpOnly Secure SameSite=Lax cookie with Referrer-Policy: no-referrer and the query string is never logged, but the parameter itself only disappears when #591 lands the one-time code exchange.
TWO SIGN-IN REDIRECT LOOPS found and fixed after enabling auth (v0.7.1 and v0.7.2). Both presented as a page that never loads, with nothing in the log, which is why they were worth hunting rather than retrying.
LOOP 1 (v0.7.1): a session cookie that iamnim REJECTS. The dashboard redirected to login, login went to iamnim, iamnim returned a session, and the cycle repeated. Reachable in normal operation because iamnim keeps sessions in memory on a single replica, so any restart invalidates every session it has issued. Now the dead cookie is cleared and the page says the sign-in did not stick, with a manual link. No redirect.
LOOP 2 (v0.7.2): the cookie never arriving AT ALL. This is the tablet case: Safari and iPadOS restrict cookies written during a cross-site navigation, and private browsing or a cookie block does the same. With no cookie there was no way to tell a first visit from a failed sign-in, so it bounced to login forever. /dashboard/authed now redirects to /dashboard?signedin=1; that marker carries no secret and exists only to separate the two cases. A missing cookie plus the marker renders a page explaining the browser did not keep the cookie.
Both are covered by tests that assert the response is NOT a redirect, and both tests were confirmed to fail against the previous code. Testbook gained checks 9, 10 and 11, including a real iPad sign-in whose pass criterion is that it never spins forever.
DESIGN NOTE for #591: a one-time code exchange does not remove either loop. Any browser that refuses the cookie fails the same way, so the marker stays useful regardless.
RAISED IN PRIORITY by #597 shipping live data on 2026-08-31. /dashboard is still anonymous, and as of hydraorganization v0.5.1 it now serves live operational data: per-venue head and body counts, and which experiences are deployed where. The venue roster and the reliability figures were already public from their own services, but the per-venue fleet posture was not. This issue is the gate. Until it lands the fallback is to remove the dashboard block from /srv/scales/hydraorganization/config.yaml, which reverts the page to example data. Also note for the design here: hydraorganization currently holds full-power ADMIN tokens for hydracluster and hydraexperiencelibrary because neither offers a read-only scope, and it only ever issues GETs. A read scope on both would shrink the blast radius of this service.