Skip to content

fix: re-acquire session tokens via the login-session token route (#117) - #118

Merged
nycomp merged 1 commit into
mainfrom
fix/117-login-session-token
Oct 27, 2026
Merged

nycomp merged 1 commit into
mainfrom
fix/117-login-session-token

Conversation

@nycomp

@nycomp nycomp commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #117 — brings the SDK in line with campus#963, resolved campus-side by campus#964 (POST /auth/v1/logins/<session_id>/token, invariant A10, merged to weekly 2026-10-26).

Problem

get_token() and Campus._get_token_from_session() resolved the user's campus bearer through the credentials management blueprint, which campus#854 made operator-only. Every downstream-app with_user_session() / auth.get_token() therefore 403'd — campus#963's exact impact (campus-classroom enrollment bridge, broker release, Send-to-Classroom all failing while browser login worked).

Change

The session-token path now uses the session-bound re-acquisition lane, per the rollout note on campus#963 — one endpoint call, no fallback to the credentials path:

  • logins.token() (new): POST /auth/v1/logins/{session_id}/token — note the canonical path has no trailing slash (route rule is /<session_id>/token, unlike the CRUD subresources; Route trailing slash compliance: Add trailing slashes to subresource ID paths #13's canonicalization doesn't apply). Auth is the client's Basic credentials plus the session id; user principals are denied outright by the route.
  • Campus._get_token_from_session() collapses from three operator-gated calls (credentials GET → refresh grant → credentials PATCH) to that one call. force_refresh / refresh_if_expired are gone: the server reuses a live grant token and rotates an expired one itself, so re-calling the endpoint is the refresh.
  • The 401 hook (feat(client): optional 401 auto-refresh hook so user-session apps can drop proactive is_expired() checks #89) re-calls the endpoint after revoke_session() — still required, since presenting the stale user bearer to the new route is a 403.
  • AuthRoot.get_token() delegates to the same lane. Server-side invalid_grant (gone session, revoked grant, lapsed refresh chain) surfaces as APIError with oauth_error="invalid_grant" — the same re-login cue as the feat(auth): refresh grant + device-flow parity for public clients — retire campus-cli's last raw token calls #87 public-client refresh.
  • Relock campus-suite to weekly@50e69c9 (includes campus#964). This also surfaced a latent skew test: test_model_without_user_field_falls_back asserted the installed campus-suite lacked the #879 user field, and its own message said it needed a pre-#879 fixture once that stopped being true — it now builds that fixture directly.

The Credentials resource class stays (operator/management surface); the SDK just stops walking it for user sessions. Retrieval matrix after this (per campus#963): app-as-user → login-session token route; upstream tokens → broker; CLI → device flow; service-to-service → client_credentials; ops inspection → credentials blueprint.

Tests

  • test_token_refresh.py: rewritten for the new lane — one endpoint call with the exact path, response parsing (incl. the user_id echo surviving as provider_fields), no-session raises without HTTP, invalid_grant mapping, and a _get_token_from_session delegation test asserting zero calls to the credentials/refresh/PATCH legs. Public-client auth.refresh(stored) tests untouched.
  • test_unauthorized_refresh.py: hook assertions updated (re-acquisition carries no kwargs now).
  • Full suite: 295 passed, poetry run pyright clean (CI parity: poetry run pytest + poetry run pyright).

Rollout

Downstream consumers unblock against dev once this merges (dev auth already runs weekly with the route). Per the #956 relock pattern, campus-side can relock campus-api-python to the merge commit afterwards; campus-classroom and other with_user_session() callers need no code changes.

get_token() and _get_token_from_session() resolved the user's campus
bearer through the credentials management routes, which campus#854 made
operator-only — every downstream with_user_session() 403'd (campus#963).
Campus ratified the fix in campus#964: POST /auth/v1/logins/<session_id>/
token, session-bound re-acquisition (invariant A10).

- logins.token() adds the session-bound lane: one Basic-authenticated
  endpoint call; the server reuses a live grant token or rotates an
  expired one itself; the response carries no refresh token
- _get_token_from_session() collapses from three operator-gated calls
  (credentials GET -> refresh grant -> credentials PATCH) to that one
  call, with no fallback to the credentials path; force_refresh /
  refresh_if_expired are gone — re-calling the endpoint is the refresh
- the 401 hook re-calls the endpoint after revoke_session() (the route
  denies user principals outright)
- get_token() delegates to the same lane

Relock campus-suite to weekly@50e69c9 (includes campus#964) and adapt
the #879 skew test to a pre-#879 model fixture instead of depending on
an old install.
@nycomp
nycomp merged commit 84d5800 into main Oct 27, 2026
5 checks passed
@nycomp
nycomp deleted the fix/117-login-session-token branch October 27, 2026 02:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

get_token() resolves the campus token via the operator-gated credentials route — follow campus#963/#964 login-session token lane

2 participants