Skip to content

Commit 21a3c98

Browse files
ArthurC-Ninaclaude
andcommitted
fix(auth): absolutize relative Keycloak verification URIs in device flow
netcup's Keycloak returns relative verification_uri(_complete) (e.g. /realms/scp/device), which printed an unusable path. Normalize to absolute URLs against BASE_HOST and synthesize the complete URL when missing; clearer login output. Adds a unit test for the normalizer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b18d87f commit 21a3c98

2 files changed

Lines changed: 37 additions & 7 deletions

File tree

netcup_api/auth.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ class DeviceCode:
3333
interval: int
3434

3535

36+
def _absolutize(uri: str | None, fallback: str) -> str:
37+
"""Turn a possibly-relative verification URI into an absolute URL."""
38+
if not uri:
39+
return fallback
40+
if uri.startswith("http://") or uri.startswith("https://"):
41+
return uri
42+
return config.BASE_HOST.rstrip("/") + "/" + uri.lstrip("/")
43+
44+
3645
def _write_secure(path, data: dict[str, Any]) -> None:
3746
config.ensure_config_dir()
3847
tmp = path.with_suffix(path.suffix + ".tmp")
@@ -88,11 +97,19 @@ def start_device_flow(self) -> DeviceCode:
8897
if r.status_code != 200:
8998
raise AuthError(f"device auth failed: HTTP {r.status_code}: {r.text}")
9099
d = r.json()
100+
user_code = d["user_code"]
101+
# Keycloak may return *relative* verification URIs (e.g. "/realms/scp/device").
102+
# Normalize them to absolute URLs so they are usable in a browser.
103+
verify = _absolutize(d.get("verification_uri"), config.DEVICE_VERIFICATION_URL)
104+
verify_complete = _absolutize(d.get("verification_uri_complete"), "")
105+
if not verify_complete:
106+
sep = "&" if "?" in verify else "?"
107+
verify_complete = f"{verify}{sep}user_code={user_code}"
91108
return DeviceCode(
92109
device_code=d["device_code"],
93-
user_code=d["user_code"],
94-
verification_uri=d.get("verification_uri", config.DEVICE_VERIFICATION_URL),
95-
verification_uri_complete=d.get("verification_uri_complete", ""),
110+
user_code=user_code,
111+
verification_uri=verify,
112+
verification_uri_complete=verify_complete,
96113
expires_in=int(d.get("expires_in", 600)),
97114
interval=int(d.get("interval", 5)),
98115
)
@@ -129,12 +146,16 @@ def poll_for_token(
129146

130147
def login(self, open_browser: bool = True, printer: Callable[[str], None] = print) -> dict:
131148
dc = self.start_device_flow()
132-
url = dc.verification_uri_complete or dc.verification_uri
133-
printer(f"Open this URL in your browser and approve access:\n\n {url}\n")
134-
printer(f"User code: {dc.user_code}\n")
149+
printer(
150+
"Open this URL in your browser and approve access:\n\n"
151+
f" {dc.verification_uri_complete}\n\n"
152+
"If the code is not pre-filled, go to:\n\n"
153+
f" {dc.verification_uri}\n\n"
154+
f"and enter this code: {dc.user_code}\n"
155+
)
135156
if open_browser:
136157
try:
137-
webbrowser.open(url)
158+
webbrowser.open(dc.verification_uri_complete)
138159
except Exception: # noqa: BLE001 - headless is fine
139160
pass
140161
printer("Waiting for authorization...")

tests/test_spec.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
"""Offline tests for the OpenAPI-driven registry (no network, no auth)."""
22

3+
from netcup_api.auth import _absolutize
34
from netcup_api.spec import _make_operation_id, load
45

56

7+
def test_absolutize_verification_uri():
8+
base = "https://www.servercontrolpanel.de"
9+
assert _absolutize("/realms/scp/device", "fb") == f"{base}/realms/scp/device"
10+
assert _absolutize("https://h/p", "fb") == "https://h/p"
11+
assert _absolutize("", "fallback") == "fallback"
12+
assert _absolutize(None, "fallback") == "fallback"
13+
14+
615
def test_operation_id_generation():
716
assert _make_operation_id("get", "/api/v1/servers") == "get-servers"
817
assert _make_operation_id("get", "/api/v1/servers/{serverId}") == "get-servers-by-serverId"

0 commit comments

Comments
 (0)