{"id":"authenticated-rest-api-call","version":"1.0.0","primitive":"tool_calling/api","description":"Make authenticated REST API calls using Bearer token and API key header patterns","registry_refs":["httpx","requests"],"tags":["http","rest-api","authentication","bearer-token","api-key","httpx","requests"],"solves":["token in URL instead of header","missing Authorization prefix","sync httpx instead of requests","no timeout set","credentials in logs"],"auth_required":true,"verified":true,"last_verified":"2026-04-14","next_check":"2026-07-14","eval_result":"pass","eval_env":"python3.12/linux","mast":["FM-1.1","FM-1.5","FM-2.2","FM-2.4","FM-2.6","FM-3.2","FM-3.3"],"ref":"https://arxiv.org/abs/2503.13657","executable":"# ============================================\n# checklist:     authenticated-rest-api-call\n# version:       1.0.0\n# primitive:     tool_calling/api\n# description:   Make authenticated REST API calls using Bearer token and API key header patterns\n# registry_refs: httpx, requests\n# auth_required: true\n# verified:      true\n# last_verified: 2026-04-14\n# next_check:    2026-07-14\n# eval_result:   pass\n# eval_env:      python3.12/linux\n#\n# tags:   http, rest-api, authentication, bearer-token, api-key, httpx, requests\n# solves: token in URL instead of header, missing Authorization prefix, sync httpx instead of requests, no timeout set, credentials in logs\n# mast: FM-1.1, FM-1.5, FM-2.2, FM-2.4, FM-2.6, FM-3.2, FM-3.3\n# ref:  https://arxiv.org/abs/2503.13657\n#\n# INPUTS:\n#   MOCK_BEARER_TOKEN — string, Bearer token to test with (default: \"test-bearer-token-abc123\")\n#   MOCK_API_KEY      — string, API key to test with (default: \"test-api-key-xyz789\")\n#   TEST_URL          — string, echo endpoint that returns request headers (default: \"https://httpbin.org/headers\")\n#\n# OUTPUTS:\n#   bearer_auth_verified — bool, Bearer token sent with correct \"Bearer \" prefix\n#   apikey_verified      — bool, X-API-Key header sent and echoed correctly\n#   credentials_in_url   — bool, always False (tokens must never appear in URL)\n#   redaction_verified   — bool, auth headers redacted before logging\n# ============================================\n\nimport os\nimport sys\nimport json\nimport subprocess\nimport requests as _requests\n\n# ----------------------------------------\n# PRE_EXECUTION\n# FM-2.2: fetch ground truth for all registry_refs\n# ----------------------------------------\n\nREGISTRY_REFS = [\"httpx\", \"requests\"]\nMAX_RETRIES = 2\nregistries = {}\n\nfor lib in REGISTRY_REFS:\n    for attempt in range(MAX_RETRIES):\n        try:\n            response = _requests.get(\n                f\"https://checklist.day/api/registry/{lib}\",\n                timeout=10\n            )\n            if response.status_code == 200:\n                registries[lib] = response.json()\n                break\n        except _requests.exceptions.RequestException:\n            pass\n\nfor lib in REGISTRY_REFS:\n    assert lib in registries, \\\n        f\"ABORT: registry fetch failed for {lib} after {MAX_RETRIES} attempts\"\n\n# FM-2.4: surface breaking warnings — do not withhold\nfor lib, registry in registries.items():\n    breaking = [\n        w for w in registry.get(\"warnings\", [])\n        if w.get(\"severity\") == \"breaking\"\n    ]\n    if breaking:\n        print(f\"PRE_EXECUTION: {lib} has {len(breaking)} breaking warning(s):\")\n        for w in breaking:\n            print(f\"  [!] [{w.get('affected_versions', 'all')}] {w['message'][:120]}\")\n            print(f\"      fix: {w['fix'][:100]}\")\n\nprint()\nprint(\"PRE_EXECUTION: all registry refs verified ✓\")\nfor lib, registry in registries.items():\n    install = registry.get(\"install\", [{}])[0].get(\"cmd\", \"unknown\")\n    print(f\"  {lib:20s} : {install}\")\n\n# ----------------------------------------\n# KNOWN FAILURE MODES\n#\n# 1. Token in URL — NEVER do: requests.get(url + \"?token=\" + token)\n#    Tokens in URLs end up in server logs, browser history, referrer headers\n#\n# 2. Missing \"Bearer \" prefix — Authorization header must be \"Bearer <token>\"\n#    not just the raw token string\n#\n# 3. Using httpx.get() for sync calls — correct for sync, but\n#    httpx.AsyncClient is required inside async contexts (e.g. FastAPI, asyncio)\n#    Using sync httpx inside async will block the event loop\n#\n# 4. No timeout — agents hang indefinitely on unresponsive APIs\n#    Always set timeout explicitly\n#\n# 5. Credentials in logs — never log headers dict directly, redact first\n# ----------------------------------------\n\nDEFAULT_TIMEOUT = 10  # seconds — always set explicitly\n\n\ndef redact_headers(headers: dict) -> dict:\n    \"\"\"FM-2.4: never log raw auth headers — redact before surfacing.\"\"\"\n    safe = {}\n    for k, v in headers.items():\n        if k.lower() in (\"authorization\", \"x-api-key\", \"api-key\", \"x-auth-token\"):\n            safe[k] = \"[REDACTED]\"\n        else:\n            safe[k] = v\n    return safe\n\n\ndef make_bearer_request(url: str, token: str, method: str = \"GET\", **kwargs):\n    \"\"\"\n    Pattern 1: Bearer token auth via requests.\n    FM-2.6: Authorization header format is 'Bearer <token>' — not just the token.\n    FM-1.5: explicit timeout — never hang indefinitely.\n    \"\"\"\n    import requests\n\n    headers = {\n        \"Authorization\": f\"Bearer {token}\",  # FM-2.6: prefix required\n        \"Accept\": \"application/json\",\n        \"Content-Type\": \"application/json\",\n    }\n\n    response = requests.request(\n        method=method,\n        url=url,\n        headers=headers,\n        timeout=DEFAULT_TIMEOUT,  # FM-1.5: always set\n        **kwargs\n    )\n\n    return response\n\n\ndef make_apikey_request(url: str, api_key: str, method: str = \"GET\", **kwargs):\n    \"\"\"\n    Pattern 2: API key header auth via httpx (sync).\n    Common header names: X-API-Key, Api-Key, x-api-key — check provider docs.\n    FM-2.6: use httpx for sync; use httpx.AsyncClient for async contexts.\n    FM-1.5: explicit timeout.\n    \"\"\"\n    import httpx\n\n    headers = {\n        \"X-API-Key\": api_key,  # adjust header name per provider\n        \"Accept\": \"application/json\",\n    }\n\n    response = httpx.request(\n        method=method,\n        url=url,\n        headers=headers,\n        timeout=DEFAULT_TIMEOUT,  # FM-1.5: always set\n        **kwargs\n    )\n\n    return response\n\n\ndef handle_auth_error(response) -> None:\n    \"\"\"\n    FM-3.2: check auth before parsing response body.\n    401 = bad/expired token. 403 = valid token, insufficient permissions.\n    Both are distinct — handle separately.\n    \"\"\"\n    if response.status_code == 401:\n        raise PermissionError(\n            \"ABORT: 401 Unauthorized — token is missing, expired, or malformed. \"\n            \"Check token value and 'Bearer ' prefix.\"\n        )\n    if response.status_code == 403:\n        raise PermissionError(\n            \"ABORT: 403 Forbidden — token is valid but lacks required scope/permissions. \"\n            \"Check API key scopes or OAuth grant.\"\n        )\n\n\n# ----------------------------------------\n# EXECUTION\n# Demonstrate both patterns against httpbin.org — no real API key needed\n# httpbin echoes back the request headers so we can verify auth was sent correctly\n# FM-1.1: idempotent GET requests — safe to run multiple times\n# ----------------------------------------\n\ntry:\n    import requests\nexcept ImportError:\n    pkg = registries[\"requests\"][\"install\"][0][\"cmd\"].replace(\"pip install \", \"\").strip()\n    print(f\"\\nEXECUTION: requests not found — installing {pkg}...\")\n    subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", pkg])\n    import requests\n\ntry:\n    import httpx\nexcept ImportError:\n    pkg = registries[\"httpx\"][\"install\"][0][\"cmd\"].replace(\"pip install \", \"\").strip()\n    print(f\"\\nEXECUTION: httpx not found — installing {pkg}...\")\n    subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", pkg])\n    import httpx\n\nTEST_URL = \"https://httpbin.org/headers\"\nMOCK_BEARER_TOKEN = \"test-bearer-token-abc123\"\nMOCK_API_KEY = \"test-api-key-xyz789\"\n\nprint()\nprint(\"EXECUTION: Pattern 1 — Bearer token via requests...\")\nresponse_bearer = make_bearer_request(TEST_URL, MOCK_BEARER_TOKEN)\nhandle_auth_error(response_bearer)\nbearer_data = response_bearer.json()\nprint(f\"  status  : {response_bearer.status_code}\")\nprint(f\"  headers sent: {json.dumps(redact_headers(bearer_data['headers']), indent=4)}\")\n\nprint()\nprint(\"EXECUTION: Pattern 2 — API key header via httpx...\")\nresponse_apikey = make_apikey_request(TEST_URL, MOCK_API_KEY)\nhandle_auth_error(response_apikey)\napikey_data = response_apikey.json()\nprint(f\"  status  : {response_apikey.status_code}\")\nprint(f\"  headers sent: {json.dumps(redact_headers(apikey_data['headers']), indent=4)}\")\n\n# ----------------------------------------\n# POST_EXECUTION\n# FM-3.2: verify auth headers were actually sent — not just that request succeeded\n# FM-3.3: exact match on header values\n# ----------------------------------------\n\n# Verify Bearer token was sent with correct format\nsent_auth = bearer_data[\"headers\"].get(\"Authorization\", \"\")\nassert sent_auth == f\"Bearer {MOCK_BEARER_TOKEN}\", \\\n    f\"FAIL: Authorization header mismatch — got '{sent_auth}'\"\nassert sent_auth.startswith(\"Bearer \"), \\\n    \"FAIL: Authorization header missing 'Bearer ' prefix\"\n\n# Verify API key header was sent\nsent_apikey = apikey_data[\"headers\"].get(\"X-Api-Key\", \"\")\nassert sent_apikey == MOCK_API_KEY, \\\n    f\"FAIL: X-API-Key header mismatch — got '{sent_apikey}'\"\n\n# Verify token is NOT in URL (common footgun)\nassert MOCK_BEARER_TOKEN not in response_bearer.request.url, \\\n    \"FAIL: token found in URL — credentials must be in headers only\"\nassert MOCK_API_KEY not in str(response_apikey.request.url), \\\n    \"FAIL: api key found in URL — credentials must be in headers only\"\n\n# Verify redaction works — credentials must not appear in logs\nredacted = redact_headers({\"Authorization\": f\"Bearer {MOCK_BEARER_TOKEN}\", \"X-API-Key\": MOCK_API_KEY})\nassert redacted[\"Authorization\"] == \"[REDACTED]\", \\\n    \"FAIL: Authorization header not redacted\"\nassert redacted[\"X-API-Key\"] == \"[REDACTED]\", \\\n    \"FAIL: X-API-Key header not redacted\"\n\nprint()\nprint(\"POST_EXECUTION: Bearer token format verified ✓  ('Bearer ' prefix present)\")\nprint(\"POST_EXECUTION: API key header verified ✓\")\nprint(\"POST_EXECUTION: credentials not in URL ✓\")\nprint(\"POST_EXECUTION: header redaction verified ✓\")\n\nresult = {\n    \"status\": \"pass\",\n    \"bearer_auth_verified\": True,\n    \"apikey_verified\": True,\n    \"credentials_in_url\": False,\n    \"redaction_verified\": True,\n}\nprint(result)\nprint(\"PASS\")\n"}