Usage ===== This package provides a Keycloak OIDC device flow handler and an httpx auth class that injects bearer tokens automatically. Quick start ----------- Using the package with httpx is straightforward. Create a device flow handler and a token cache, then create an ``HttpxDeviceFlowAuth`` instance with them. Pass the auth instance to an httpx client: .. code-block:: python from acmadauth.handlers import KeycloakOIDCDeviceFlowHandler from acmadauth.tokencaches import MemoryTokenCache from pprint import pprint from acmadauth.auth import HttpxDeviceFlowAuth import httpx realm = "https://auth.acmad.cloud.edu.au/realms/ACMADCI" client_id = "acmad-ci" handler = KeycloakOIDCDeviceFlowHandler(realm, client_id) cache = MemoryTokenCache() auth = HttpxDeviceFlowAuth(handler, cache, open_browser=True) client = httpx.Client(auth=auth) resp = client.get("https://acmad-ci.acmad.cloud.edu.au/api/v1/user/current_user/") print(f"Request Status Code: {resp.status_code}") pprint(resp.json()) If you prefer to use requests, you can use the ``RequestsDeviceFlowAuth`` class instead: .. code-block:: python from acmadauth.handlers import KeycloakOIDCDeviceFlowHandler from acmadauth.tokencaches import MemoryTokenCache from pprint import pprint from acmadauth.auth import RequestsDeviceFlowAuth import requests realm = "https://auth.acmad.cloud.edu.au/realms/ACMADCI" client_id = "acmad-ci" handler = KeycloakOIDCDeviceFlowHandler(realm, client_id) cache = MemoryTokenCache() auth = RequestsDeviceFlowAuth(handler, cache, open_browser=True) resp = requests.get("https://acmad-ci.acmad.cloud.edu.au/api/v1/user/current_user/", auth=auth) print(f"Request Status Code: {resp.status_code}") pprint(resp.json()) Usage with keyring token cache ------------------------------ .. code-block:: python from acmadauth.handlers import KeycloakOIDCDeviceFlowHandler from acmadauth.tokencaches import KeyringTokenCache from pprint import pprint from acmadauth.auth import HttpxDeviceFlowAuth import httpx realm = "https://auth.acmad.cloud.edu.au/realms/ACMADCI" client_id = "acmad-ci" handler = KeycloakOIDCDeviceFlowHandler(realm, client_id) cache = KeyringTokenCache("acmad:oidc:ci-instance") auth = HttpxDeviceFlowAuth(handler, cache, open_browser=True) client = httpx.Client(auth=auth) resp = client.get("https://acmad-ci.acmad.cloud.edu.au/api/v1/user/current_user/") print(f"Request Status Code: {resp.status_code}") pprint(resp.json()) Usage in a GUI application -------------------------- .. code-block:: python # /// script # requires-python = ">=3.12" # dependencies = [ # "acmadauth>=1.3.1", # "pypng>=0.20220715.0", # "pyqrcode>=1.2.1", # ] # # [[tool.uv.index]] # url = "https://pypi.acmad.cloud.edu.au/" # /// import threading import tkinter as tk from tkinter import messagebox import httpx import pyqrcode from acmadauth.auth import HttpxDeviceFlowAuth from acmadauth.handlers import KeycloakOIDCDeviceFlowHandler from acmadauth.tokencaches import MemoryTokenCache REALM_URL = "https://auth.acmad.cloud.edu.au/realms/ACMADCI" CLIENT_ID = "acmad-ci" USER_ENDPOINT = "https://acmad-ci.acmad.cloud.edu.au/api/v1/user/current_user/" def main() -> None: root = tk.Tk() root.title("ACMAD Login") root.resizable(False, False) status_var = tk.StringVar(value="Click Login to authenticate.") status_label = tk.Label(root, textvariable=status_var, padx=12, pady=8) status_label.pack() url_label = tk.Label(root, text="", wraplength=320, justify="center", padx=12) url_label.pack() qr_label = tk.Label(root, padx=12, pady=8) qr_label.pack() login_button = tk.Button(root, text="Login") login_button.pack(pady=10) def set_qr_from_url(url: str) -> None: status_var.set("Scan the QR code or complete login in the browser.") url_label.config(text=url) qr = pyqrcode.create(url) png_base64 = qr.png_as_base64_str(scale=5) image = tk.PhotoImage(data=png_base64) qr_label.image = image qr_label.config(image=image) def login_url_callback(url: str) -> None: root.after(0, set_qr_from_url, url) def set_idle_state() -> None: status_var.set("Click Login to authenticate.") url_label.config(text="") qr_label.config(image="") qr_label.image = None login_button.config(state=tk.NORMAL) def set_busy_state() -> None: status_var.set("Starting authentication...") login_button.config(state=tk.DISABLED) def show_success(first_name: str, last_name: str) -> None: messagebox.showinfo( "Authenticated", f"Authenticated as {first_name} {last_name}.", ) set_idle_state() def show_error(message: str) -> None: messagebox.showerror("Authentication failed", message) set_idle_state() def auth_worker() -> None: try: handler = KeycloakOIDCDeviceFlowHandler(REALM_URL, CLIENT_ID) cache = MemoryTokenCache() auth = HttpxDeviceFlowAuth( handler, cache, open_browser=True, show_qr=False, show_url=False, login_url_callback=login_url_callback, ) with httpx.Client(auth=auth) as client: resp = client.get(USER_ENDPOINT) resp.raise_for_status() data = resp.json() first_name = data.get("first_name", "") last_name = data.get("last_name", "") root.after(0, show_success, first_name, last_name) except Exception as exc: root.after(0, show_error, str(exc)) def on_login() -> None: set_busy_state() thread = threading.Thread(target=auth_worker, daemon=True) thread.start() login_button.config(command=on_login) root.mainloop() if __name__ == "__main__": main() To run this exmaple, save it to a file named ``gui_example.py`` and run it with uv: .. code-block:: bash pip install uv uv run gui_example.py Device authorisation flow ------------------------- When tokens are missing or expired, the device flow is started. The handler prints a verification URL and optionally a QR code. You can also open a browser tab automatically with ``open_browser=True``.