From a4201236350009e80985a980646e65570142f040 Mon Sep 17 00:00:00 2001 From: cleverhoods Date: Sat, 20 Jun 2026 22:05:10 +0200 Subject: [PATCH 01/21] fix(auth): report upstream edge challenge instead of misleading OAuth-not-configured error --- UNRELEASED.md | 2 + .../interfaces/cli/auth_command.py | 25 +++++++ tests/unit/test_auth_edge_challenge.py | 75 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 tests/unit/test_auth_edge_challenge.py diff --git a/UNRELEASED.md b/UNRELEASED.md index e93f97cd..23c45f9c 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -6,4 +6,6 @@ ### Fixed +- auth: `ails auth login` now identifies an upstream edge challenge (e.g. a Cloudflare interstitial in front of the auth endpoint) as the real cause instead of reporting a generic HTTP error or a misleading "OAuth not configured" message. Both the client-id lookup and the token-exchange step recognize the challenge page and tell you it is not fixable in the CLI — retry shortly or contact support. + ### Removed diff --git a/src/reporails_cli/interfaces/cli/auth_command.py b/src/reporails_cli/interfaces/cli/auth_command.py index d5c74327..22cfa9fe 100644 --- a/src/reporails_cli/interfaces/cli/auth_command.py +++ b/src/reporails_cli/interfaces/cli/auth_command.py @@ -38,6 +38,20 @@ class PlatformUnavailableError(Exception): """The Reporails platform's auth surface couldn't be reached or returned an unexpected response.""" +# Markers identifying a Cloudflare edge interstitial (managed challenge / JS challenge). +_EDGE_CHALLENGE_MARKERS = ( + "Just a moment", + "Attention Required! | Cloudflare", + "challenge-platform", + "cf-mitigated", +) + + +def _is_edge_challenge(body: str) -> bool: + """Detect a Cloudflare edge-challenge interstitial in a response body.""" + return any(marker in body for marker in _EDGE_CHALLENGE_MARKERS) + + def _user_agent() -> str: """User-Agent string for outbound auth requests to the platform. @@ -123,6 +137,11 @@ def _resolve_client_id(base_url: str) -> str: if resp.status_code != 200: logger.warning("Platform returned HTTP %s for client-id", resp.status_code) + if _is_edge_challenge(resp.text): + raise PlatformUnavailableError( + "Reporails platform is behind an upstream Cloudflare edge challenge — " + "this is not fixable in the CLI. Retry shortly or contact support@reporails.com.", + ) raise PlatformUnavailableError( f"Reporails platform returned HTTP {resp.status_code} for client-id endpoint.", ) @@ -292,6 +311,12 @@ def login( ) raise typer.Exit(1) from exc except (httpx.HTTPError, OSError) as exc: + if isinstance(exc, httpx.HTTPStatusError) and _is_edge_challenge(exc.response.text): + console.print( + " [red]Upstream Cloudflare edge challenge[/] — not fixable in the CLI. " + "Retry shortly or contact support@reporails.com.", + ) + raise typer.Exit(1) from exc console.print(f" [red]Failed to exchange token:[/] {exc}") raise typer.Exit(1) from exc diff --git a/tests/unit/test_auth_edge_challenge.py b/tests/unit/test_auth_edge_challenge.py new file mode 100644 index 00000000..866efac4 --- /dev/null +++ b/tests/unit/test_auth_edge_challenge.py @@ -0,0 +1,75 @@ +"""Offline coverage for the Cloudflare edge-challenge disambiguation in auth. + +The live test (`test_auth_live.py`) only exercises the happy path. These mock +the network hop so the challenge branch — the regression REQ-213 fixes — is +guarded without a network round-trip. +""" + +from __future__ import annotations + +import httpx +import pytest + +from reporails_cli.interfaces.cli.auth_command import ( + PlatformUnavailableError, + _is_edge_challenge, + _resolve_client_id, +) + + +@pytest.mark.unit +@pytest.mark.subsys_api +@pytest.mark.parametrize( + "body", + [ + "Just a moment...", + "Attention Required! | Cloudflare", + '