py_oidc_auth_client#
A lightweight OIDC/OAuth 2.0 client library for authenticating against
servers that use py-oidc-auth (or any standard OIDC provider).
The main entry point is the authenticate() function which handles
token caching, refresh, and interactive login automatically.
Quick start#
from py_oidc_auth_client import authenticate
# Interactive login (opens browser, returns token)
token = authenticate(host="https://myapp.example.com")
print(token["access_token"][:20], "...")
# Use the token with any HTTP client
import httpx
resp = httpx.get(
"https://myapp.example.com/api/data",
headers=token["headers"],
)
Non interactive / batch mode#
# On your workstation: login once and save the token
token = authenticate(host="https://myapp.example.com")
# On the cluster or in CI: reuses cached token automatically
token = authenticate(host="https://myapp.example.com")
Multiple servers#
Tokens are cached per host, so authenticating to different servers does not overwrite previous tokens:
token_a = authenticate(host="https://server-a.example.com")
token_b = authenticate(host="https://server-b.example.com")
For finer control over individual flows, see DeviceFlow
and CodeFlow.
CLI usage#
python -m py_oidc_auth_client https://myapp.example.com
python -m py_oidc_auth_client https://myapp.example.com --timeout 120
python -m py_oidc_auth_client --list
python -m py_oidc_auth_client --clear
- exception py_oidc_auth_client.AuthError(message: str, detail: Dict[str, Any] | None = None, status_code: int = 500)#
Bases:
ExceptionAuthentication or token exchange failed.
- Parameters:
message (str) – Human readable summary of what went wrong.
detail (dict, optional) – Raw error payload from the OIDC provider, typically containing
"error"and"error_description"keys.status_code (int) – HTTP status code from the upstream response. Defaults to 500 when the error did not originate from an HTTP call.
- message#
The error summary.
- Type:
str
- detail#
Provider error payload (empty dict if not available).
- Type:
dict
- status_code#
HTTP status code.
- Type:
int
Examples
Catching authentication failures:
from py_oidc_auth_client import authenticate from py_oidc_auth_client.exceptions import AuthError try: token = authenticate(host="https://myapp.example.com") except AuthError as exc: print(f"Login failed ({exc.status_code}): {exc}")
- class py_oidc_auth_client.BaseFlow(host: str = '', config: Config | None = None, **kwargs: Any)#
Bases:
objectShared token lifecycle, caching, and persistence.
This class is not meant to be instantiated directly. Use
DeviceFloworCodeFlowinstead, or the convenience functionauthenticate().Instances are singletons per host: calling
DeviceFlow("https://a.example.com")twice returns the same object, butDeviceFlow("https://b.example.com")creates a separate instance.Tokens are persisted in a per host JSON store (
TokenStore). Expired entries are evicted automatically on every read.- Parameters:
host (str) – Base URL of the application server.
config (Config or None) – Full configuration object. When provided, host and all keyword arguments are ignored.
store (TokenStore or None) – Custom token store instance. A shared default store is used when
None.login_route (str) – Server path for the authorization code login endpoint.
token_route (str) – Server path for the token exchange endpoint.
device_route (str) – Server path for the device authorization endpoint.
redirect_ports (list of int or None) – Ports to try for the local callback server (code flow only).
timeout (int or None) – HTTP and polling timeout in seconds.
Notes
The per host singleton pattern ensures that only one instance per concrete subclass and host exists. Token state is shared between
DeviceFlowandCodeFlowfor the same host via theTokenStore.Examples
Per host singletons:
a = DeviceFlow("https://host-a.example.com") b = DeviceFlow("https://host-a.example.com") c = DeviceFlow("https://host-b.example.com") assert a is b # same host -> same instance assert a is not c # different host -> different instance
Shared token cache across flows:
device = DeviceFlow("https://myapp.example.com") code = CodeFlow("https://myapp.example.com") token = await device.authenticate() assert code.token is not None # same store, same host
- async refresh(refresh_token: str) Token#
Obtain a new access token using a refresh token.
- Parameters:
refresh_token (str) – The refresh token from a previous authentication.
- Returns:
A fresh token with updated expiry times.
- Return type:
- Raises:
AuthError – If the refresh request fails (e.g. token revoked).
Examples
flow = DeviceFlow("https://myapp.example.com") new_token = await flow.refresh(old_token["refresh_token"])
- classmethod reset_instances() None#
Clear all cached singleton instances.
Primarily useful in tests to ensure a clean state.
Examples
DeviceFlow.reset_instances()
- property session: AsyncClient#
Lazy
httpx.AsyncClientshared across all requests.
- property token: Token | None#
The cached token for this host, or
None.Reads from the
TokenStore. Expired entries are pruned automatically.- Returns:
The cached token, or
Noneif no valid entry exists.- Return type:
Token or None
- class py_oidc_auth_client.CodeFlow(host: str = '', config: Config | None = None, **kwargs: Any)#
Bases:
BaseFlowOAuth 2.0 Authorization Code Grant with PKCE.
Opens the provider’s login page in a local browser, starts a temporary HTTP server to capture the callback, and exchanges the authorization code for tokens.
Best suited for desktop and local development environments where a browser is available on the same machine.
- Parameters:
host (str) – Base URL of the application server.
config (Config or None) – Full configuration object. Overrides host and keyword arguments.
store (TokenStore or None) – Custom token store. Shared default when
None.timeout (int or None) – Maximum seconds to wait for the user to complete the browser login.
Examples
from py_oidc_auth_client.flows import CodeFlow flow = CodeFlow("https://myapp.example.com", timeout=120) token = await flow.authenticate() print(token["access_token"][:20], "...")
See also
DeviceFlowHeadless/CLI device authorization flow.
authenticateTop level convenience function.
- async authenticate(*, force: bool = False) Token#
Run the authorization code flow and return a token.
Checks cached tokens first. Falls back to a browser based login when no valid tokens are available.
- Parameters:
force (bool) – Skip the cache and force a new interactive login.
- Returns:
The authentication token.
- Return type:
- Raises:
AuthError – If the login times out or the code exchange fails.
Examples
flow = CodeFlow("https://myapp.example.com") token = await flow.authenticate()
- class py_oidc_auth_client.Config(host: str, redirect_ports: List[int] = <factory>, token_env_var: str = 'OIDC_TOKEN_FILE', app_name: str = 'py-oidc-auth', login_route: str = '/auth/v2/login', token_route: str = '/auth/v2/token', device_route: str = '/auth/v2/device')#
Bases:
objectConnection and routing configuration for an OIDC enabled server.
- Parameters:
host (str) – Base URL of the application server (e.g.
"https://myapp.example.com").redirect_ports (list of int, optional) – Ports to try when starting a local HTTP server for the authorization code callback. The first available port is used. Defaults to
[53100, 53101, 53102, 53103, 53104, 53105].token_env_var (str) – Name of the environment variable that points to a token file.
app_name (str) – Application name used to locate the platform specific cache directory for token storage (via
platformdirs).login_route (str) – Server side route for the authorization code login endpoint.
token_route (str) – Server side route for the token exchange endpoint.
device_route (str) – Server side route for the device authorization endpoint.
Examples
from py_oidc_auth_client.utils import Config cfg = Config( host="https://myapp.example.com", app_name="my-project", login_route="/auth/v2/login", token_route="/auth/v2/token", device_route="/auth/v2/device", )
- app_name: str = 'py-oidc-auth'#
- device_route: str = '/auth/v2/device'#
- host: str#
- login_route: str = '/auth/v2/login'#
- redirect_ports: List[int]#
- token_env_var: str = 'OIDC_TOKEN_FILE'#
- token_route: str = '/auth/v2/token'#
- class py_oidc_auth_client.DeviceCode#
Bases:
TypedDictDevice authorization response.
Returned by
get_device_code(). Passdevice_codeandintervaltopoll()to complete the flow.- uri#
Verification URL the user should open in a browser. This is
verification_uri_completewhen the provider supports it, otherwiseverification_uri.- Type:
str
- user_code#
Short code the user enters at the verification URI (e.g.
"ABCD-EFGH").- Type:
str
- device_code#
Opaque code used to poll the token endpoint.
- Type:
str
- interval#
Minimum polling interval in seconds.
- Type:
int
Examples
Manual device flow:
from py_oidc_auth_client import DeviceFlow flow = DeviceFlow("https://myapp.example.com") code = await flow.get_device_code() print(f"Open {code['uri']} and enter: {code['user_code']}") token = await flow.poll( device_code=code["device_code"], interval=code["interval"], )
- device_code: str#
- interval: int#
- uri: str#
- user_code: str#
- class py_oidc_auth_client.DeviceFlow(host: str = '', config: Config | None = None, **kwargs: Any)#
Bases:
BaseFlowOAuth 2.0 Device Authorization Grant (RFC 8628).
Best suited for CLI tools, headless servers, CI jobs, and environments where opening a local browser is not possible. The user visits a URL on any device, enters a short code, and the client polls until approval.
- Parameters:
host (str) – Base URL of the application server.
config (Config or None) – Full configuration object. Overrides host and keyword arguments.
store (TokenStore or None) – Custom token store. Shared default when
None.timeout (int or None) – Maximum seconds to wait for user approval during polling.
interactive (bool or None) – Whether to show a spinner and attempt to open the browser.
Noneauto detects based on the terminal environment.
Examples
Fully automatic login (opens browser, polls, returns token):
from py_oidc_auth_client.flows import DeviceFlow flow = DeviceFlow("https://myapp.example.com", timeout=120) token = await flow.authenticate() print(token["access_token"][:20], "...")
Step by step control:
flow = DeviceFlow("https://myapp.example.com") # Step 1: get device code code = await flow.get_device_code() print(f"Open {code['uri']}") print(f"Enter code: {code['user_code']}") # Step 2: poll until user approves token = await flow.poll( device_code=code["device_code"], interval=code["interval"], )
Reusing a cached token:
flow = DeviceFlow("https://myapp.example.com") if flow.token: print("Already authenticated!") else: token = await flow.authenticate()
See also
CodeFlowBrowser based authorization code flow.
authenticateTop level convenience function.
- async authenticate(*, force: bool = False, auto_open: bool = True) Token#
Run the full device flow and return a token.
Checks cached tokens first. When force is
Falseand a valid access token exists, it is returned immediately. When only the refresh token is still valid, a refresh is attempted before falling back to a full device flow.- Parameters:
force (bool) – Skip the cache and force a new interactive login.
auto_open (bool) – Attempt to open the verification URL in the default browser.
- Returns:
The authentication token.
- Return type:
- Raises:
AuthError – If the flow fails, times out, or the user denies access.
Examples
flow = DeviceFlow("https://myapp.example.com") token = await flow.authenticate()
- async get_device_code() DeviceCode#
Request a new device code from the authorization server.
- Returns:
Contains
uri,user_code,device_code, andinterval. Displayurianduser_codeto the user, then callpoll()withdevice_codeandinterval.- Return type:
- Raises:
AuthError – If the device authorization endpoint is unavailable or returns an invalid response.
Examples
flow = DeviceFlow("https://myapp.example.com") code = await flow.get_device_code() print(f"Go to: {code['uri']}") print(f"Code: {code['user_code']}")
- async poll(device_code: str, interval: int = 5) Token#
Poll the token endpoint until the user approves or denies.
- Parameters:
device_code (str) – The
device_codeobtained fromget_device_code().interval (int) – Minimum seconds between poll requests. Use the value from
get_device_code()to respect the provider’s rate limit.
- Returns:
The authentication token after user approval.
- Return type:
- Raises:
AuthError – On timeout, denial (
access_denied), or token expiry.
Examples
code = await flow.get_device_code() # ... show code to user ... token = await flow.poll( code["device_code"], code["interval"], )
- class py_oidc_auth_client.Token#
Bases:
TypedDictOAuth 2.0 token payload.
- access_token#
The bearer access token (JWT).
- Type:
str
- token_type#
Token type, typically
"Bearer".- Type:
str
- expires#
Access token expiry as a Unix timestamp (seconds).
- Type:
int
- refresh_token#
The refresh token for obtaining new access tokens.
- Type:
str
- refresh_expires#
Refresh token expiry as a Unix timestamp (seconds).
- Type:
int
- scope#
Space separated list of granted scopes.
- Type:
str
- headers#
Pre built
Authorizationheader ready for use with HTTP clients (e.g.{"Authorization": "Bearer eyJ..."})- Type:
dict of str to str
Examples
Using the token with
httpx:from py_oidc_auth_client import authenticate token = authenticate(host="https://myapp.example.com") headers = token["headers"] import httpx resp = httpx.get( "https://myapp.example.com/api/data", headers=headers, )
- access_token: str#
- expires: int#
- headers: Dict[str, str]#
- refresh_expires: int#
- refresh_token: str#
- scope: str#
- token_type: str#
- class py_oidc_auth_client.TokenStore(path: str | Path | None = None, app_name: str = 'py-oidc-auth')#
Bases:
objectPer host token cache with automatic TTL eviction.
Each host gets its own entry in the store. Stale entries are pruned lazily on every
get()call.- Parameters:
path (str or Path or None) – Path to the JSON cache file. Defaults to the platform cache directory (
~/.cache/py-oidc-auth/token-store.jsonon Linux).app_name (str) – Application name for the cache directory. Only used when path is
None.
Examples
Basic usage:
from py_oidc_auth_client.token_store import TokenStore store = TokenStore() # Save a token for a host store.put("https://myapp.example.com", token) # Retrieve it later (returns None if expired) cached = store.get("https://myapp.example.com") if cached: print("Cache hit!") # See all cached hosts for host in store.hosts(): print(host) # Remove a specific host store.remove("https://myapp.example.com")
Custom file location:
store = TokenStore("~/.config/myapp/tokens.json")
Custom app directory:
store = TokenStore(app_name="my-project") # -> ~/.cache/my-project/token-store.json
- clear() None#
Remove all cached tokens.
Examples
store = TokenStore() store.clear()
- get(host: str) Token | None#
Look up a cached token for host.
Triggers a cleanup pass that removes all expired entries from the store file.
- Parameters:
host (str) – The server URL to look up.
- Returns:
The cached token if it exists and is not expired, or
Noneotherwise.- Return type:
Token or None
Examples
store = TokenStore() token = store.get("https://myapp.example.com") if token: headers = token["headers"]
- hosts() List[str]#
Return the list of hosts that have cached tokens.
Expired entries are pruned before building the list.
- Returns:
Normalised host URLs.
- Return type:
list of str
Examples
store = TokenStore() for host in store.hosts(): print(host)
- put(host: str, token: Token) None#
Store a token for host, overwriting any previous entry.
- Parameters:
host (str) – The server URL the token belongs to.
token (Token) – The token to cache.
Examples
store = TokenStore() store.put("https://myapp.example.com", token)
- remove(host: str) bool#
Remove the cached token for host.
- Parameters:
host (str) – The server URL to remove.
- Returns:
Trueif an entry was removed,Falseif not found.- Return type:
bool
Examples
store = TokenStore() removed = store.remove("https://myapp.example.com")
- py_oidc_auth_client.authenticate(host: str, *, login_route: str = '/auth/v2/login', token_route: str = '/auth/v2/token', device_route: str = '/auth/v2/device', redirect_ports: List[int] | None = None, store: TokenStore | None = None, app_name: str = 'py-oidc-auth', force: bool = False, timeout: int | None = 30) Token#
Authenticate to an OIDC protected server and return a token.
This is the primary entry point for the library. It handles the full authentication lifecycle:
Check the token store for a cached token for this host.
If the access token is still valid, return it immediately.
If only the refresh token is valid, perform a token refresh.
Otherwise, start an interactive login. The device flow is attempted first; if the server does not support it, the authorization code flow (local browser) is used as a fallback.
Tokens are stored per host in a shared JSON file, so authenticating to multiple servers does not overwrite previous tokens.
- Parameters:
host (str) – Base URL of the application server (e.g.
"https://myapp.example.com").login_route (str) – Server side route for the authorization code login endpoint.
token_route (str) – Server side route for the token exchange endpoint.
device_route (str) – Server side route for the device authorization endpoint.
redirect_ports (list of int, optional) – Ports to try when starting a local HTTP server for the authorization code callback. The first available port is used. Defaults to
[53100, 53101, 53102, 53103, 53104, 53105].store (TokenStore or None) – Custom
TokenStorefor token persistence. WhenNonea default store is created in the platform cache directory (e.g.~/.cache/<app_name>/token-store.json).app_name (str) – Application name for the cache directory. Only used when store is
None. Defaults to"py-oidc-auth".force (bool) – When
True, skip the cache and force a fresh interactive login even if a valid token exists.timeout (int or None) – Maximum seconds to wait for the user to complete the browser login.
Nonewaits indefinitely.
- Returns:
A token dictionary with keys
access_token,refresh_token,expires,refresh_expires,scope,token_type, andheaders.The
headersvalue is a ready to use dict:{"Authorization": "Bearer eyJ..."}.- Return type:
- Raises:
AuthError – If authentication fails (timeout, user denial, server error, or no interactive session available in a batch environment).
Examples
Basic interactive login:
from py_oidc_auth_client import authenticate token = authenticate(host="https://myapp.example.com") print(token["access_token"][:20])
Authenticate to multiple servers:
token_a = authenticate(host="https://server-a.example.com") token_b = authenticate(host="https://server-b.example.com") # Both tokens are cached independently
Force re authentication:
token = authenticate( host="https://myapp.example.com", force=True, timeout=120, )
Custom app name (changes cache directory):
token = authenticate( host="https://myapp.example.com", app_name="my-project", ) # Tokens stored in ~/.cache/my-project/token-store.json
Custom token store:
from py_oidc_auth_client import TokenStore, authenticate store = TokenStore("~/.config/myapp/tokens.json") token = authenticate( host="https://myapp.example.com", store=store, )
- async py_oidc_auth_client.authenticate_async(host: str, *, login_route: str = '/auth/v2/login', token_route: str = '/auth/v2/token', device_route: str = '/auth/v2/device', redirect_ports: List[int] | None = None, store: TokenStore | None = None, app_name: str = 'py-oidc-auth', force: bool = False, timeout: int | None = 30) Token#
Async version of
authenticate().Identical behaviour but can be awaited from an existing event loop (e.g. inside a Jupyter notebook or an async application).
- Parameters:
host (str) – Base URL of the application server.
login_route (str) – Server side route for the authorization code login endpoint.
token_route (str) – Server side route for the token exchange endpoint.
device_route (str) – Server side route for the device authorization endpoint.
redirect_ports (list of int, optional) – Ports to try when starting a local HTTP server for the authorization code callback. The first available port is used. Defaults to
[53100, 53101, 53102, 53103, 53104, 53105].store (TokenStore or None) – Custom token store. Shared default when
None.app_name (str) – Application name for the cache directory.
force (bool) – Skip the cache and force a fresh login.
timeout (int or None) – Maximum seconds to wait for user approval.
- Returns:
The authentication token.
- Return type:
- Raises:
AuthError – If authentication fails.
Examples
from py_oidc_auth_client import authenticate_async token = await authenticate_async( host="https://myapp.example.com", timeout=120, )
See also
authenticateSynchronous wrapper for non async code.