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:
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:
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
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
# /// 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:
pip install uv
uv run gui_example.py