Skip to content

feat(auth): refresh grant + device-flow parity for public clients - #88

Merged
nycomp merged 1 commit into
mainfrom
feat/oauth-public-client
Oct 4, 2026
Merged

nycomp merged 1 commit into
mainfrom
feat/oauth-public-client

Conversation

@nycomp

@nycomp nycomp commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

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 sends client_id — the token endpoint requires it for every grant (campus/auth/routes/oauth.py token()), and the client previously shipped the refresh grant without it, so the request could only fail server-side (422 missing-parameter). Explicit client_id arg for public clients; CLIENT_ID env fallback covers server mode (_get_token_from_session).
  • New 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 as APIError with oauth_error in 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 the AUTH_* code via new errors.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".
  • New auth.oauth.wait_for_token() implements the polling loop the CLI owned in login.py: persisted slow_down (+5s per RFC 8628 §3.5), authorization_pending polling with an on_pending hook for progress UI, network/5xx retry on non-final attempts, and a timeout error when max_attempts is exhausted. sleep is injectable for tests.
  • Parity table (error mapping + slow_down semantics) documented in the oauth module 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.refresh rotation 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.

…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).
@nycomp
nycomp merged commit 057e991 into main Oct 4, 2026
2 checks passed
@nycomp
nycomp deleted the feat/oauth-public-client branch October 4, 2026 05:56
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.

feat(auth): refresh grant + device-flow parity for public clients — retire campus-cli's last raw token calls

2 participants