angee.integrate.oauth.client
Stateless OAuth2 protocol bound to one OAuthClient registration row.
The base of the connection protocol: authorization-code redirect, code exchange, refresh, and revocation — everything needed to connect an external account for API access (Gemini, Grok, Anthropic), with no identity/login concern. OIDC login extends this in angee.iam_integrate_oidc.protocol.OAuthClientOidcProtocol.
The OAuth2 protocol mechanism (token request, client authentication, PKCE, and the token-response parsing) is owned by Authlib's OAuth2Client over httpx; this module is the thin per-row adapter behind a stable seam. The authorization URL is still built here (a deterministic string the OIDC layer extends), and the small token_request_format == "json" provider quirk — a non-standard JSON token body Authlib does not emit, plus Anthropic's required state echo — keeps a documented shim.
Endpoints are taken from the row as configured; when a row has a discovery URL, the protocol asks the row to fill missing OAuth endpoints before failing. OIDC login extends this in iam_integrate_oidc for ID-token/userinfo verification.
OAuthClientProtocol
class OAuthClientProtocol()OAuth2 authorization-code + refresh protocol for one OAuthClient row.
__init__
def __init__(oauth_client: Any) -> NoneBind the protocol to one OAuth client registration row.
authorize_url
def authorize_url(*,
state: str,
redirect_uri: str,
scopes: Iterable[str],
code_challenge: str | None = None) -> strReturn the provider authorization URL for one OAuth code flow.
exchange_code
def exchange_code(*,
code: str,
redirect_uri: str,
code_verifier: str | None = None,
state: str | None = None) -> dict[str, Any]Exchange an authorization code for token material.
state is part of the redirect seam. The standard form path validates it before exchange and leaves it out of the token request (RFC 6749 §4.1.3); the JSON shim carries it because Anthropic's public-client token endpoint rejects that non-standard JSON request as malformed without it.
refresh_token
def refresh_token(*, refresh_token: str) -> dict[str, Any]Exchange a stored refresh token for fresh token material (RFC 6749 §6).
The provider may rotate the refresh token; when it returns a new one the caller persists it. Raises OAuthFlowError when the grant is rejected.
fetch_userinfo
def fetch_userinfo(access_token: str) -> dict[str, Any]Best-effort fetch of profile claims with one OAuth access token.
Reads the access-token-protected userinfo_endpoint to label a connected account (connect needs no ID token). The OIDC layer overrides this to resolve the endpoint from discovery first.
revoke_token
def revoke_token(token: str) -> NoneBest-effort RFC 7009 token revocation for an OAuth credential.
ensure_endpoints
def ensure_endpoints() -> dict[str, Any]Ask the OAuth client row to fill endpoint fields from discovery.