Repository navigation
feat(auth): refresh grant + device-flow parity for public clients - #88
Merged
Merged
Conversation
…87) - auth.token(refresh_token grant) now sends client_id — required by the token endpoint for every grant (campus auth/routes/oauth.py); explicit arg for public clients, CLIENT_ID env fallback for server mode. The grant previously shipped without client_id and could only fail server-side. - auth.refresh(stored) — public-client helper: presents the stored (single-use) refresh token and returns the rotated pair. - auth.oauth.wait_for_token() — RFC 8628 §3.5 polling loop: persisted slow_down (+5s), authorization_pending polling with on_pending hook, network retry on non-final attempts, timeout error. Parity table in the oauth module docstring lets campus-cli retire login.py's copies. - poll_for_token() parses all three server error shapes: Campus envelope (dev), envelope with details stripped (production — OAuth error recovered from the AUTH_* code via errors.oauth_error_from_code), and flat RFC 6749. - 218 tests (was 197).
This was referenced Oct 4, 2026
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.
Closes #87.
Implements items 1 and 2 of the issue so campus-cli can retire its last raw token calls (
refresh_access_token,login.py's device loop). Item 3 (JsonClient-level 401 auto-refresh) is deferred to a follow-up issue — it is not needed for the CLI retirement and has retry-semantics questions of its own (replaying non-idempotent POSTs, hook configuration).1. Refresh-token grant for public clients
auth.token(grant_type="refresh_token")now sendsclient_id— the token endpoint requires it for every grant (campus/auth/routes/oauth.pytoken()), and the client previously shipped the refresh grant without it, so the request could only fail server-side (422 missing-parameter). Explicitclient_idarg for public clients;CLIENT_IDenv fallback covers server mode (_get_token_from_session).auth.refresh(stored: OAuthToken, *, client_id=None) -> OAuthToken— the issue's suggested shape: takes the stored token, presents its (single-use, rotating) refresh token, returns the new pair. Errors surface asAPIErrorwithoauth_errorin details — the caller's cue to fall back to re-login, mirroring campus-cli#29's best-effort pattern.2. Device-flow parity
poll_for_token()now parses all three error shapes the server emits: the Campus envelope{"error": {code, message, details.oauth_error}}(dev/staging), the same envelope with details stripped in production (OAuth error recovered from theAUTH_*code via newerrors.oauth_error_from_code()), and the flat RFC 6749 form. Previously only the flat form was recognized — against the real server every 400 parsed as "Unknown error".auth.oauth.wait_for_token()implements the polling loop the CLI owned inlogin.py: persistedslow_down(+5s per RFC 8628 §3.5),authorization_pendingpolling with anon_pendinghook for progress UI, network/5xx retry on non-final attempts, and a timeout error whenmax_attemptsis exhausted.sleepis injectable for tests.oauthmodule docstring.Tests
218 passing (was 197): envelope/flat/stripped error mapping, polling-loop semantics (persisted slow_down, fatal errors, network retry, timeout), refresh-grant body contract,
auth.refreshrotation and failure mapping.Consumer note: the actual campus-cli migration PR (retiring
campus_cli/auth/common.py::refresh_access_token+login.py's loop) can now proceed against these APIs.