Authentication Flows#

OIDC/OAuth 2.0 client side authentication flows.

The module provides three classes arranged in an inheritance hierarchy:

BaseFlow          (token lifecycle, persistence, refresh)
  |
  +-- DeviceFlow  (OAuth 2.0 Device Authorization Grant)
  +-- CodeFlow    (OAuth 2.0 Authorization Code Grant with PKCE)

All flow classes share a process wide token cache via BaseFlow so that repeated calls reuse existing tokens when possible.

Quick start#

Most users should use the top level authenticate() function which picks the right flow automatically. The classes in this module are useful when you need explicit control over the flow.

from py_oidc_auth_client.flows 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(code["device_code"], code["interval"])
class py_oidc_auth_client.flows.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.flows.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.flows.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"],
)