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: Exception

Authentication 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: object

Shared token lifecycle, caching, and persistence.

This class is not meant to be instantiated directly. Use DeviceFlow or CodeFlow instead, or the convenience function authenticate().

Instances are singletons per host: calling DeviceFlow("https://a.example.com") twice returns the same object, but DeviceFlow("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 DeviceFlow and CodeFlow for the same host via the TokenStore.

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:

Token

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.AsyncClient shared 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 None if no valid entry exists.

Return type:

Token or None

class py_oidc_auth_client.CodeFlow(host: str = '', config: Config | None = None, **kwargs: Any)#

Bases: BaseFlow

OAuth 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

DeviceFlow

Headless/CLI device authorization flow.

authenticate

Top 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:

Token

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: object

Connection 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: TypedDict

Device authorization response.

Returned by get_device_code(). Pass device_code and interval to poll() to complete the flow.

uri#

Verification URL the user should open in a browser. This is verification_uri_complete when the provider supports it, otherwise verification_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: BaseFlow

OAuth 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. None auto 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

CodeFlow

Browser based authorization code flow.

authenticate

Top 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 False and 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:

Token

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, and interval. Display uri and user_code to the user, then call poll() with device_code and interval.

Return type:

DeviceCode

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_code obtained from get_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:

Token

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: TypedDict

OAuth 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 Authorization header 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: object

Per 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.json on 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 None otherwise.

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:

True if an entry was removed, False if 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:

  1. Check the token store for a cached token for this host.

  2. If the access token is still valid, return it immediately.

  3. If only the refresh token is valid, perform a token refresh.

  4. 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 TokenStore for token persistence. When None a 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. None waits indefinitely.

Returns:

A token dictionary with keys access_token, refresh_token, expires, refresh_expires, scope, token_type, and headers.

The headers value is a ready to use dict: {"Authorization": "Bearer eyJ..."}.

Return type:

Token

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:

Token

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

authenticate

Synchronous wrapper for non async code.