Repository navigation
fix: login-session token re-acquisition presents Basic only (#117) - #119
Merged
Merged
Conversation
POST /logins/{id}/token serves client principals only (campus#963/A10
guard, #854): user bearers are denied outright. token() rode whatever
Authorization header the shared session last set, so a
with_user_session() nested inside another one — an auth-broker release
reached from a route that already holds the user session, exactly the
campus-classroom enrollment checks — presented the outer session's
user token and was refused with 403 (surfacing downstream as
BrokerConfigError 502s on every enrollment-gated submission mutation).
token() now resets to the client's Basic credentials for the call and
restores any prior Authorization header afterwards (also on failure),
so an enclosing session keeps its authorization until it re-runs
use_token() with the fresh bearer. The initial acquisition path now
matches the discipline the 401-refresh hook already documented ("the
session's stale bearer must not be presented mid-rotation").
Found live: campus-classroom /a/ answer-sheet submit 502s on dev
(campus-classroom#14 verification pass, 2026-10-27).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
POST /logins/{session_id}/tokenserves client principals only — campus.auth's A10 guard (authz.forbid_user_principal, #854 lineage) denies user bearers outright. ButLogin.token()sent its request on the shared session's ambientAuthorizationheader, whatever it happened to hold.The breakage shows whenever
with_user_session()runs nested inside anotherwith_user_session(): the outer session has setAuthorization: Bearer <user token>on the shared auth client, so the inner session's re-acquisition presents a user token to the route → 403FORBIDDEN→ (in campus-classroom)BrokerConfigError→ 502 on every enrollment-gated submission mutation.Found live on dev, 2026-10-27, during the campus-classroom #14 verification pass: the
/a/answer sheet's Save-draft/Submit 502'd while the page render's identical broker release succeeded seconds earlier — the render path enterswith_user_sessionwith a fresh (Basic) header because the preceding app-scoped read resets it; the mutation path enters nested.The SDK's own 401-refresh hook already documented the discipline this route needs — "the login-session token route denies user principals outright (campus#964/A10), so the session's stale bearer must not be presented mid-rotation" (
refresh_bearer()revokes first) — the initial acquisition path just didn't follow it.Fix
Login.token()now:Authorizationheader,reset_authorization()→ the client's Basic credentials for the call,finally, so the enclosing session keeps its authorization even when the route answers 403/400.The enclosing
with_user_session()immediately overwrites the header viause_token(<fresh bearer>), so restore-then-overwrite is correct in both the top-level and nested cases.Tests
TestLoginTokenPresentsBasicOnly(4 cases, mock-client contract style matching the file):reset_authorization → POST /logins/{id}/token → set_bearer_authorization(<prior>)when a bearer is ambient,set_bearer.Full suite: 321 passed.
Impact on consumers
After this, nested
with_user_session()blocks work; the outer session's subsequent calls may ride the inner session'sfinallyreset (Basic) and self-heal through the existing 401 hook (+1 round trip) — the same healing path an expired bearer already takes.