Authentication¶
The SDK speaks to Campus Auth for every token concern and holds no session state of its own. Pick the lane that matches your host:
| Lane | Who | What you use |
|---|---|---|
| Server mode | backend services | CLIENT_ID/CLIENT_SECRET Basic auth; with_app_session() for app-identity calls |
| Device flow | CLIs, public clients | oauth.request_device_code() + wait_for_token() |
| User sessions | Flask apps acting as the logged-in user | with_user_session() (401 auto-refresh on by default) |
| Token release | confidential clients needing upstream tokens | broker.token() (token_bridge clients) |
Server mode¶
Server-to-server calls authenticate with the OAuth client's Basic
credentials (CLIENT_ID / CLIENT_SECRET). You can drive tokens
manually — auth.token() for a
client_credentials grant, then
use_token() — or wrap the work in
the app-session context manager, which acquires and releases the token
for you:
with campus.with_app_session() as client:
campus.api.timetable.set_current(timetable_id)
# Authorization is cleared on exit
Device flow (public clients)¶
Public clients (the CLI, desktop tools) have no secret. The RFC 8628 flow walks the user through a verification page:
from campus_python import Campus, errors
campus = Campus(mode="device")
code = campus.auth.oauth.request_device_code(client_id="campus-cli")
print("Enter this code:", code["user_code"], "at", code["verification_uri"])
token = campus.auth.oauth.wait_for_token(
client_id="campus-cli",
device_code=code["device_code"],
interval=code.get("interval"),
on_pending=lambda: print(".", end=""), # progress hook per poll
)
campus.use_token(token)
wait_for_token() implements the full polling loop: authorization_pending
keeps polling, slow_down raises the interval by 5s for the rest of
the flow, network/5xx failures retry until the final attempt, and
every other error is fatal. Error responses are normalized to
AuthenticationError with
the OAuth error code readable via .oauth_error, across all three
shapes the auth server emits (dev envelope, production
details-stripped envelope, flat RFC 6749).
Refresh-token grants rotate server-side: present a stored token once
via auth.refresh(stored)
and persist the returned pair before reusing the stale one.
Revoke with oauth.revoke()
(RFC 7009 — the server answers 200 whether or not the token existed).
User sessions¶
A login session (created by the browser flow in campus.flask_campus
via logins.new())
lets server code act as that user. Inside the session context manager
every call carries the user's bearer, and an expired bearer is
re-acquired once automatically:
with campus.with_user_session() as client: # 401 auto-refresh ON
campus.api.submissions["sub_ab12"].unsubmit()
with campus.with_user_session(refresh_on_401=False) as client: # legacy behaviour
...
How the pieces work:
- The session id resolves explicit argument > installed provider
(
set_session_id_provider(); flask_campus installs one). With neither, methods raiseAuthenticationErrornaming the seam. auth.get_token()/logins.token()re-acquire the session user's bearer viaPOST /auth/v1/logins/<session_id>/token— one Basic-authenticated call; an expired grant token is rotated server-side, a missing one raisesinvalid_grant(re-login required).- The 401 hook retries the original request once with the fresh
bearer. It cannot double-execute work: Campus authenticates in a
before_requesthook before any handler runs, so a 401 means no handler executed.
Manual bearer control¶
Any client can be pointed at a token you obtained elsewhere (e.g. a CLI storing tokens in a keyring):
campus.api.client.set_bearer_authorization(access_token)
campus.auth.client.set_bearer_authorization(access_token)
# ...
campus.revoke_session() # clear Authorization on both
use_token() sets both roots in one
call. Raw CampusRequest users (public clients driving
auth.refresh) can install their own 401 hook:
client.set_unauthorized_hook(
lambda: campus.auth.refresh(stored, client_id="campus-cli").access_token
)
Upstream tokens (broker)¶
Confidential clients flagged token_bridge can release the bearer's
upstream access tokens (never refresh tokens) for Google and other
connected providers:
token = campus.auth.broker.token("google", integration="classroom",
min_scopes=["classroom.rosters.read"])
Denials are typed: 403 carries details.missing_scopes, 401 maps to
AuthenticationError. Users inspect and revoke their own grants via
connections
(connections["google"].delete(user_id=...) — disconnecting the
campus provider itself is logout and is rejected server-side).
Hosting in a Flask app¶
The SDK owns transport and tokens; the campus.flask_campus
middleware (in the campus repo)
owns the browser login flow and client-side session storage. The two
layers join through injectable providers:
| Concern | Owner | How the SDK sees it |
|---|---|---|
| Login flow (authorize / code exchange / logout) | campus.flask_campus.context |
orchestrates this SDK's pure API calls (sessions.new, exchange_code_for_token, logins.new, ...) |
| Session-id storage (browser session keys) | campus.flask_campus.context |
set_session_id_provider() — ids resolve explicit argument > provider |
| Request-context trace headers | campus.flask_campus.propagation |
tracing.set_request_context_provider() — see Tracing |
| User-session token lane | this SDK | with_user_session() — pure orchestration; the session id arrives via the provider above |
Non-Flask hosts need none of this: the CLI authenticates via device flow, stores tokens in its own keyring, and sets bearers directly.