Skip to main content
Everything on this page is imported from charter.auth, not charter. OAuth 2.0 declared rather than wrapped: a server is a frozen declaration in RFC 8414’s vocabulary, a client refreshes a grant you already hold, and a flow covers the two steps of obtaining one that are protocol rather than product. The host keeps the callback route, the session, and storage. Charter signs nothing, so JWT-bearer and service-account grants are out of scope.

OAuth2Server

str
required
Where the form POST goes. Empty raises DeclarationError.
"client_secret_post" | "client_secret_basic"
default:"\"client_secret_post\""
How the client authenticates (RFC 6749 §2.3.1). client_secret_post puts the credentials in the form body, which is what Google, GitHub and most APIs expect; client_secret_basic puts them in an HTTP Basic header. Any other value raises DeclarationError.
Optional[str]
default:"None"
The issuer identifier, when the server publishes one.
Optional[str]
default:"None"
Where a user is sent for consent. Optional: a declaration without one is still a working token-refresh-only server, and OAuth2Flow.authorize raises with instructions if you start a consent flow against one.
Mapping[str, str]
default:"{}"
Extra authorization-URL parameters this registration needs. The one field discovery can never fill in — Google without {"access_type": "offline", "prompt": "consent"} returns no refresh token and reports no error. Normalised to a read-only, key-sorted mapping, so the declaration stays frozen all the way down.

OAuth2Server.discover

Read the server’s own metadata and build a declaration from it. Tries .well-known/openid-configuration, then .well-known/oauth-authorization-server. This is the path for anything an enterprise runs — Okta, Entra ID, Auth0, Keycloak and Ping all publish one. token_endpoint_auth_method is chosen from token_endpoint_auth_methods_supported, preferring client_secret_post. A document with no token_endpoint, or advertising neither usable method, raises CredentialError; so does exhausting both well-known paths, with every attempt named in the message. The one network call in this library that is not a tool call.

OAuth2Client

A CredentialProvider backed by a token endpoint. Holds one grant — one end user, or one machine identity. Serve many users by wrapping it in SubjectProvider.
OAuth2Server
required
The declaration to refresh against. Anything else raises TypeError.
str
required
Your registration’s client id. Empty raises CredentialError.
str
required
Your registration’s client secret. Empty raises CredentialError.
Optional[str]
default:"None"
The grant to renew. Required for the refresh_token grant — omitting it raises CredentialError saying that obtaining a grant is your application’s flow.
"refresh_token" | "client_credentials"
default:"\"refresh_token\""
refresh_token is the agent acting for an end user; client_credentials is the agent acting as itself. These are the two grants that are a plain form POST; any other value raises ValueError.
Optional[str]
default:"None"
Space-separated scopes to send with the token request, when the server wants them narrowed.
Optional[Callable[[Credentials, Optional[str]], None | Awaitable[None]]]
default:"None"
Called after every successful refresh with the new credentials and the refresh token to store next time — which may be a new one, since some servers rotate on every use. This is where persistence lives. A callback that raises is logged at WARNING and does not fail the call the token was fetched for.
int
default:"90"
How early to renew. Capped at half the lifetime the server granted, so a 60-second token does not refresh on every call.
int
default:"20"
Timeout for the token request.
Optional[httpx.AsyncClient]
default:"None"
Reuse an HTTP client for token requests. When omitted, one is created and closed per request.
The access token is cached in memory until the leeway window and written nowhere. Concurrent callers wait on a single refresh: some servers invalidate the previous refresh token on use, so twelve parallel tool calls each firing their own refresh would poison eleven of them. An invalid_grant answer marks the grant dead for 60 seconds and re-raises the same error without asking the server again — one revoked user must not become a stream of requests against an endpoint that rate-limits per client.

Attributes and methods

OAuth2Server
The declaration passed in.
"refresh_token" | "client_credentials"
Which grant this client uses.
Optional[str]
The scope string sent with token requests.
int
The configured leeway, before the lifetime cap is applied.
Optional[str]
The current refresh token, which is not necessarily the one passed in: a server that rotates hands back a new one on every refresh and Charter adopts it.
Credentials
The provider protocol. Returns the cached token, or refreshes.
None
Forget the cached token and any dead-grant cool-down. Call it after re-authorizing, if you are reusing the client rather than building a new one.

OAuth2Client.from_grant

A client seeded from a fresh exchange: the grant’s access token pre-fills the cache, so the first tool call after connecting spends no refresh. A grant carrying no refresh token is refused with CredentialError — for a one-shot access token, use StaticTokenProvider.

OAuth2Flow

The stateless half of obtaining a grant. Registration facts live here — client_id, client_secret, redirect_uri — beside the server declaration, which holds what the server is.
str
required
The exact callback URL registered with the authorization server, never one built from user input. Empty raises CredentialError.
Nothing is remembered between authorize() and exchange(): the state and the PKCE verifier are handed to you and taken back, because only your framework knows which browser is which.

OAuth2Flow.authorize

Build the authorization URL (RFC 6749 §4.1.1). Pure — no I/O, no state.
Iterable[str]
required
What to ask for. Empty raises CredentialError: an authorization request for nothing is a bug upstream. scopes_for derives this from a tool set.
Optional[str]
default:"None"
Your own CSRF token. Generated with secrets.token_urlsafe(32) when omitted.
Optional[str]
default:"None"
Which account to pre-select on the consent screen.
bool
default:"True"
S256 PKCE (RFC 7636). False exists for the rare server that rejects unknown parameters, and is never the recommended path.
Optional[Mapping[str, str]]
default:"None"
Per-request additions, merged last.
Parameters merge later-wins: the standard set, then the server’s authorization_params, then extra_params. Neither map may set client_id, redirect_uri, state or anything starting with code_challenge — identity and the CSRF/PKCE material belong to the flow, and an attempt raises ValueError naming the source. A server with no authorization_endpoint raises CredentialError. scope is encoded with %20 rather than +, which some servers reject.

OAuth2Flow.exchange

Trade the callback’s code for tokens (RFC 6749 §4.1.3). One form POST to the same token endpoint the refresh uses.
str
required
The authorization code from the callback. Empty raises CredentialError.
Optional[str]
default:"None"
The verifier authorize() returned, when PKCE was used.
bool
default:"True"
Raise CredentialError when the server returns no refresh token. This is the silent failure of the whole flow — the exchange succeeds, the access token works, and the integration dies within the hour. Pass False only for a server that genuinely never issues one.
Codes are single-use, so a failed exchange is never retried and no cooldown applies; restarting the flow is the host’s move.

AuthorizationRequest

What authorize() hands back.
str
Redirect the user here.
str
Put it in the session, server-side and keyed to this browser, and compare it at the callback with states_match.
Optional[str]
Keep it beside the state and pass it to exchange(). None when pkce=False.

TokenGrant

What a code exchange returned — the one moment a refresh token is visible.
str
Valid now, typically for an hour.
Optional[str]
What you persist. None when the server issued none and expect_refresh_token=False.
Optional[datetime]
Derived from expires_in, or None when the server sent none.
list[str]
What the server says was actually granted, which is not always what was asked for: some consent screens let a user deselect.
Dict[str, Any]
The untouched response body, for vendor extras.
Both repr and str mask the tokens, so printing a grant does not put a secret in a log line. This is deliberately not Credentials: that is the injection currency, handed out on every tool call. A grant is what you persist once.

states_match

Whether the callback’s state is the one authorize() issued. Compares in constant time, so the CSRF check is one obvious call rather than a plain == that leaks how many leading characters matched. Verifying state at the callback is the host’s job: it is what keeps an attacker from splicing their code into your user’s session.

scopes_for

The scopes an authorization request must cover to run these tools, read from the scopes metadata each was declared with and deduped in first-seen order.
The consent screen is then exactly what the agent can do, kept in lockstep with the tool set instead of maintained beside it.

Types in these signatures

The three aliases the signatures above are declared in. They are exported from charter so a host writing its own wrapper can name them without importing from a submodule.

Grant

Which OAuth 2.0 grant an OAuth2Client uses. "refresh_token" acts for a user who consented; "client_credentials" acts as the application itself, with no user and so no refresh token to store.

TokenEndpointAuthMethod

How the client authenticates to the token endpoint — credentials in the form body, or in an HTTP Basic header. RFC 8414 names this field, and a server’s own metadata usually announces which it wants; OAuth2Server.discover reads it for you. Getting it wrong is a silent-shaped failure: the server answers invalid_client, which reads like a wrong secret rather than a wrong envelope.

OnRefresh

Called after every successful refresh, with the grant that came back. Sync or async; both are awaited correctly. This is where a rotated refresh token gets persisted. A server that rotates hands you a new one in the refresh response and invalidates the old immediately, so a host that does not write it down is one restart away from a dead grant.