Skip to content

fix(server): forward model auth header and preserve precedence#1883

Draft
Pouyanpi wants to merge 2 commits into
developfrom
fix/forward-model-auth-header
Draft

fix(server): forward model auth header and preserve precedence#1883
Pouyanpi wants to merge 2 commits into
developfrom
fix/forward-model-auth-header

Conversation

@Pouyanpi
Copy link
Copy Markdown
Collaborator

@Pouyanpi Pouyanpi commented May 13, 2026

Description [Generated by GH Copilot]

This pull request improves how API keys and authorization headers are handled and prioritized when fetching models, ensuring that environment-configured API keys take precedence over incoming Authorization headers. It also adds comprehensive tests to verify this behavior for both OpenAI-compatible and other providers.

Authentication and header precedence improvements:

  • Updated fetch_models in nemoguardrails/server/schemas/utils.py to prioritize environment variable API keys over incoming Authorization headers, ensuring that configured keys are always used when present.

Testing enhancements:

  • Added and updated tests in tests/server/test_api.py and tests/server/test_schema_utils.py to verify that:
    • Environment API keys are used before forwarded Authorization headers for OpenAI providers. [1] [2]
    • Forwarded Authorization headers are used only when no environment key is set.
    • For non-OpenAI providers, forwarded auth headers are used with provider-specific header names when the environment key is unset.
    • Test setups explicitly clear or set environment variables to ensure correct precedence logic. [1] [2]

Related Issue(s)

Checklist

  • I've read the CONTRIBUTING guidelines.
  • I've updated the documentation if applicable.
  • I've added tests if applicable.
  • @mentions of the person or team responsible for reviewing proposed changes.

Summary by CodeRabbit

  • Bug Fixes

    • Improved authentication header handling to support case-insensitive lookups
    • Fixed authorization precedence: environment API keys now properly take precedence over forwarded headers
    • Enhanced compatibility with different model provider authentication methods
  • Tests

    • Added comprehensive test coverage for authentication scenarios across multiple providers

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

The PR updates authentication header handling in fetch_models to support case-insensitive header lookup and establish precedence between environment-configured API keys and caller-supplied authorization tokens, with provider-specific header name mapping.

Changes

Auth Header Forwarding and Precedence

Layer / File(s) Summary
Auth header case-insensitive lookup and forwarding logic
nemoguardrails/server/schemas/utils.py
fetch_models now performs case-insensitive scan of request_headers for Authorization values, forwards environment-configured keys using provider bearer settings, and strips/reapplies Bearer prefix for non-OpenAI providers when forwarding caller tokens.
Unit tests for fetch_models auth behavior
tests/server/test_schema_utils.py
Tests verify auth header forwarding when no environment key is set, environment key precedence over forwarded tokens, and provider-specific header name mapping (x-api-key for Anthropic).
API endpoint tests for /v1/models auth forwarding
tests/server/test_api.py
Integration tests for the /v1/models endpoint assert forwarded auth behavior without env key and verify env key precedence in the HTTP API layer.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main changes: forwarding model authentication headers and preserving the precedence of environment API keys over incoming headers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Results For Major Changes ✅ Passed Focused bug fix with minor changes (+7 net lines). Test coverage documented with specific test functions. No performance/numeric impact requiring benchmarks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/forward-model-auth-header

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nemoguardrails/server/schemas/utils.py`:
- Around line 152-154: The code uses forwarded.removeprefix("Bearer ") which is
case-sensitive and will not strip "bearer " or other case variants; update the
logic around raw_key to perform a case-insensitive prefix check on forwarded
(e.g., check forwarded.lower().startswith("bearer ")) and if true slice off the
original-length prefix from forwarded to produce raw_key (so token casing is
preserved), otherwise just strip forwarded; then continue using raw_key when
setting headers[auth_header_name] with the existing use_bearer handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c3a77fba-a65b-4934-acf2-ba5cc871ec77

📥 Commits

Reviewing files that changed from the base of the PR and between ef69220 and 952be2d.

📒 Files selected for processing (3)
  • nemoguardrails/server/schemas/utils.py
  • tests/server/test_api.py
  • tests/server/test_schema_utils.py

Comment on lines +152 to +154
raw_key = forwarded.removeprefix("Bearer ").strip()
if raw_key:
headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle case-insensitive bearer prefix normalization.

At Line 152, removeprefix("Bearer ") is case-sensitive. A valid header like authorization: bearer user-token won’t be stripped and can become an invalid provider key (e.g., x-api-key: bearer user-token).

Suggested fix
-            raw_key = forwarded.removeprefix("Bearer ").strip()
+            forwarded_value = forwarded.strip()
+            if forwarded_value[:7].lower() == "bearer ":
+                raw_key = forwarded_value[7:].strip()
+            else:
+                raw_key = forwarded_value
             if raw_key:
                 headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raw_key = forwarded.removeprefix("Bearer ").strip()
if raw_key:
headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key
forwarded_value = forwarded.strip()
if forwarded_value[:7].lower() == "bearer ":
raw_key = forwarded_value[7:].strip()
else:
raw_key = forwarded_value
if raw_key:
headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nemoguardrails/server/schemas/utils.py` around lines 152 - 154, The code uses
forwarded.removeprefix("Bearer ") which is case-sensitive and will not strip
"bearer " or other case variants; update the logic around raw_key to perform a
case-insensitive prefix check on forwarded (e.g., check
forwarded.lower().startswith("bearer ")) and if true slice off the
original-length prefix from forwarded to produce raw_key (so token casing is
preserved), otherwise just strip forwarded; then continue using raw_key when
setting headers[auth_header_name] with the existing use_bearer handling.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 13, 2026

Greptile Summary

This PR fixes auth header precedence in fetch_models: environment-configured API keys now always take priority over Authorization headers forwarded from incoming requests, and the header lookup is made case-insensitive. Tests are updated to pin OPENAI_API_KEY to an empty string so test outcomes are not influenced by the ambient environment.

  • utils.py: Introduces a case-insensitive search for the forwarded Authorization value and restructures the branching so raw_key (from env) is checked first; when no env key is present the forwarded header is translated into the provider-specific auth header with appropriate Bearer handling.
  • Tests: Adds new cases covering env-key precedence for OpenAI-compatible providers, forwarded-auth-only for OpenAI, and forwarded-auth-to-provider-header for Anthropic; existing test_list_models_forwards_auth_header is hardened by explicitly zeroing OPENAI_API_KEY.

Confidence Score: 4/5

Safe to merge; the auth-precedence fix is correct for all configured providers and the new tests cover the primary paths.

The forwarding shortcut for OpenAI-compatible providers passes the raw Authorization header through verbatim, which means a client sending a non-Bearer scheme would have that unexpected value forwarded to the upstream model API — a silent auth failure in non-standard deployments.

The conditional branch in nemoguardrails/server/schemas/utils.py lines 148-154 that handles the OpenAI-compatible forwarding path deserves a second look.

Important Files Changed

Filename Overview
nemoguardrails/server/schemas/utils.py Adds case-insensitive Authorization header lookup and restructures auth precedence so env-configured API keys always win over forwarded request headers; logic is sound for all configured providers.
tests/server/test_api.py Adds OPENAI_API_KEY empty string to the existing forwarding test to prevent env-leak interference, and adds a new integration test verifying env-key precedence; coverage is appropriate.
tests/server/test_schema_utils.py Adds three new unit tests for env-key precedence, forwarded-auth for OpenAI, and non-OpenAI provider header translation; each test isolates the relevant env var, though the Anthropic test does not clear MAIN_MODEL_BASE_URL.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[fetch_models called] --> B{env API key set?}
    B -- Yes --> C[Use env key with provider auth header]
    B -- No --> D{Forwarded Authorization header present?}
    D -- No --> E[Send request without auth headers]
    D -- Yes --> F{header name is Authorization AND use_bearer?}
    F -- Yes --> G[Forward header value as-is]
    F -- No --> H[Strip Bearer prefix from forwarded value]
    H --> I{Stripped value non-empty?}
    I -- Yes --> J[Set provider auth header with or without Bearer prefix]
    I -- No --> E
    C --> K[Apply extra_headers and send GET to provider URL]
    G --> K
    J --> K
    E --> K
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
nemoguardrails/server/schemas/utils.py:148-154
The shortcut branch for the OpenAI-compatible case only triggers when both `auth_header_name == "Authorization"` **and** `use_bearer` are true, so the forwarded value is passed through verbatim. If a client sends a non-Bearer scheme (e.g. `Authorization: ApiKey xyz`), that value lands in `headers["Authorization"]` unmodified and the upstream provider will likely reject the request with a 401. Stripping and re-wrapping with `Bearer` (as the `else` branch already does) would make the forwarding consistent with the env-key path.

```suggestion
    elif forwarded:
        raw_key = forwarded.removeprefix("Bearer ").strip()
        if raw_key:
            headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key
```

### Issue 2 of 2
tests/server/test_schema_utils.py:481
The Anthropic provider constructs its URL using `MAIN_MODEL_BASE_URL` when that variable is set. If a test environment (or a previous test) leaves `MAIN_MODEL_BASE_URL` set, this test would hit a different URL than `https://api.anthropic.com/v1/models`. Since `httpx.AsyncClient` is fully mocked, the URL doesn't affect the assertion, but explicitly clearing the variable documents the intent and guards against subtle breakage if the mock scope ever narrows.

```suggestion
    with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "", "MAIN_MODEL_BASE_URL": ""}):
```

Reviews (1): Last reviewed commit: "Preserve model auth precedence" | Re-trigger Greptile

Comment on lines +148 to +154
elif forwarded:
if auth_header_name == "Authorization" and use_bearer:
headers[auth_header_name] = forwarded
else:
raw_key = forwarded.removeprefix("Bearer ").strip()
if raw_key:
headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The shortcut branch for the OpenAI-compatible case only triggers when both auth_header_name == "Authorization" and use_bearer are true, so the forwarded value is passed through verbatim. If a client sends a non-Bearer scheme (e.g. Authorization: ApiKey xyz), that value lands in headers["Authorization"] unmodified and the upstream provider will likely reject the request with a 401. Stripping and re-wrapping with Bearer (as the else branch already does) would make the forwarding consistent with the env-key path.

Suggested change
elif forwarded:
if auth_header_name == "Authorization" and use_bearer:
headers[auth_header_name] = forwarded
else:
raw_key = forwarded.removeprefix("Bearer ").strip()
if raw_key:
headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key
elif forwarded:
raw_key = forwarded.removeprefix("Bearer ").strip()
if raw_key:
headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key
Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/server/schemas/utils.py
Line: 148-154

Comment:
The shortcut branch for the OpenAI-compatible case only triggers when both `auth_header_name == "Authorization"` **and** `use_bearer` are true, so the forwarded value is passed through verbatim. If a client sends a non-Bearer scheme (e.g. `Authorization: ApiKey xyz`), that value lands in `headers["Authorization"]` unmodified and the upstream provider will likely reject the request with a 401. Stripping and re-wrapping with `Bearer` (as the `else` branch already does) would make the forwarding consistent with the env-key path.

```suggestion
    elif forwarded:
        raw_key = forwarded.removeprefix("Bearer ").strip()
        if raw_key:
            headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key
```

How can I resolve this? If you propose a fix, please make it concise.

async def test_fetch_non_openai_provider_uses_forwarded_auth_without_env_key():
"""Forwarded auth falls back to provider-specific auth headers when env key is unset."""
mock = _mock_httpx({"data": []})
with patch.dict(os.environ, {"ANTHROPIC_API_KEY": ""}):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The Anthropic provider constructs its URL using MAIN_MODEL_BASE_URL when that variable is set. If a test environment (or a previous test) leaves MAIN_MODEL_BASE_URL set, this test would hit a different URL than https://api.anthropic.com/v1/models. Since httpx.AsyncClient is fully mocked, the URL doesn't affect the assertion, but explicitly clearing the variable documents the intent and guards against subtle breakage if the mock scope ever narrows.

Suggested change
with patch.dict(os.environ, {"ANTHROPIC_API_KEY": ""}):
with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "", "MAIN_MODEL_BASE_URL": ""}):
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/server/test_schema_utils.py
Line: 481

Comment:
The Anthropic provider constructs its URL using `MAIN_MODEL_BASE_URL` when that variable is set. If a test environment (or a previous test) leaves `MAIN_MODEL_BASE_URL` set, this test would hit a different URL than `https://api.anthropic.com/v1/models`. Since `httpx.AsyncClient` is fully mocked, the URL doesn't affect the assertion, but explicitly clearing the variable documents the intent and guards against subtle breakage if the mock scope ever narrows.

```suggestion
    with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "", "MAIN_MODEL_BASE_URL": ""}):
```

How can I resolve this? If you propose a fix, please make it concise.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Pouyanpi Pouyanpi marked this pull request as draft May 13, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant