Skip to content

fix(api): enforce configured inference bearer token - #15

Merged
CoreyLeath-code merged 6 commits into
mainfrom
fix/enforce-inference-bearer-token
Aug 3, 2026
Merged

fix(api): enforce configured inference bearer token#15
CoreyLeath-code merged 6 commits into
mainfrom
fix/enforce-inference-bearer-token

Conversation

@CoreyLeath-code

@CoreyLeath-code CoreyLeath-code commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

Makes the public inference-stub route verify a configured bearer token instead of accepting any non-empty Authorization: Bearer value.

Addresses part of #13.

Problem

POST /infer previously treated bearer authentication as header presence only. Any non-empty token could invoke the endpoint, and an unset server-side token was not detected.

Root cause

The request dependency stripped the bearer prefix but did not compare the supplied token against configured server state.

Changes

  • Read the expected token from API_BEARER_TOKEN at request time.
  • Fail closed with HTTP 503 when inference authentication is not configured.
  • Reject missing and incorrect credentials with HTTP 401.
  • Use constant-time token comparison.
  • Extend API tests for unconfigured, missing, incorrect, and valid tokens.
  • Add the required variable to .env.example and the README configuration table.

Claim hardening

No production-security claim is added. This change only enforces a shared-token boundary for the demo inference route; token rotation, identity management, and authorization scopes remain out of scope.

Reproducibility and validation

Executed locally against the branch source:

  • python -m pytest tests -q — 7 passed.
  • python -m compileall -q api tests — passed.
  • ruff check api/main.py tests/test_auth.py tests/test_inference.py tests/test_rate_limit.py — passed.

GitHub Actions remains the final clean-runner verification.

Security

The route now fails closed when no server-side bearer token is configured and does not accept arbitrary bearer values.

Risk

Low for correctly configured callers. Deployments using /infer must set API_BEARER_TOKEN; otherwise the endpoint deliberately returns 503 instead of allowing unauthenticated access.

Rollback

Revert the six commits in this PR. No database or API-schema migration is involved.

Remaining work

Issue #13 retains broader clean-clone, coverage, security-evidence, container, and evaluation work.

Summary by CodeRabbit

  • Security

    • Added bearer-token authentication for the inference endpoint.
    • Requests now return clear errors for missing configuration, missing credentials, or invalid tokens.
    • Token validation uses secure comparison practices.
  • Documentation

    • Documented the required authentication configuration and local setup guidance.
    • Clarified that inference requests are unavailable when authentication is not configured.
  • Tests

    • Expanded coverage for valid, missing, and invalid authentication scenarios.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The inference API now requires a configured bearer token, returns 503 when authentication is unset, and validates tokens with constant-time comparison. Tests and configuration documentation use API_BEARER_TOKEN.

Changes

Inference API authentication

Layer / File(s) Summary
Configure and enforce bearer authentication
.env.example, README.md, api/main.py
The API reads API_BEARER_TOKEN, returns 503 when it is unset, and validates bearer tokens with constant-time comparison.
Validate authentication behavior
tests/test_auth.py, tests/test_inference.py, tests/test_rate_limit.py
Tests cover unset, missing, and invalid tokens. Inference and rate-limit tests configure the shared token.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant _require_bearer
  participant Environment
  participant InferenceAPI
  Client->>_require_bearer: Send Authorization header
  _require_bearer->>Environment: Read API_BEARER_TOKEN
  alt Token is unset
    _require_bearer-->>Client: Return 503
  else Header or token is invalid
    _require_bearer-->>Client: Return 401
  else Token is valid
    _require_bearer->>InferenceAPI: Allow POST /infer
    InferenceAPI-->>Client: Return inference response
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes enforcing the configured inference bearer token, which is the main change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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/enforce-inference-bearer-token

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@CoreyLeath-code
CoreyLeath-code marked this pull request as ready for review August 3, 2026 15:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b55e4f0351

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/main.py
return authorization.removeprefix("Bearer ").strip()

token = authorization.removeprefix("Bearer ").strip()
if not hmac.compare_digest(token, expected_token):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Compare bearer tokens as encoded bytes

When either the configured secret or a received credential contains a non-ASCII character, Python's hmac.compare_digest() raises TypeError for str operands rather than returning False. Consequently, an operator choosing a Unicode secret makes every authenticated request fail with HTTP 500, and an ASGI server that accepts a non-ASCII header lets malformed credentials trigger the same error instead of a 401. Encode both values to bytes before comparison or explicitly reject non-ASCII credentials.

Useful? React with 👍 / 👎.

@CoreyLeath-code
CoreyLeath-code merged commit 3e1a234 into main Aug 3, 2026
15 of 16 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@api/main.py`:
- Around line 37-41: Update both 401 HTTPException branches in _require_bearer()
to include headers={"WWW-Authenticate": "Bearer"}, covering missing/invalid
authorization and token mismatch cases. Extend tests/test_auth.py to assert this
WWW-Authenticate header on both responses.
- Around line 36-39: Update the authorization parsing around the Bearer
validation and token extraction to accept case-insensitive scheme values while
preserving the existing token validation behavior. Use the proposed parser for
separating scheme and token fields, and add coverage in tests/test_auth.py for a
mixed- or lowercase header such as “bearer test-token”.

In `@README.md`:
- Line 154: Update the API_BEARER_TOKEN table description in README.md to begin
with “Shared bearer token required for” while preserving the existing endpoint
and 503 behavior details.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ca35ab6-c89b-4888-bf31-aed85ad3b944

📥 Commits

Reviewing files that changed from the base of the PR and between 558cdb1 and b55e4f0.

📒 Files selected for processing (6)
  • .env.example
  • README.md
  • api/main.py
  • tests/test_auth.py
  • tests/test_inference.py
  • tests/test_rate_limit.py

Comment thread api/main.py
Comment on lines 36 to +39
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
return authorization.removeprefix("Bearer ").strip()

token = authorization.removeprefix("Bearer ").strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^|/)(main\.py|test_auth\.py)$|api/|tests/' | sed -n '1,120p'

echo "== api/main.py around auth =="
if [ -f api/main.py ]; then
  nl -ba api/main.py | sed -n '1,80p'
fi

echo "== test_auth.py =="
if [ -f tests/test_auth.py ]; then
  nl -ba tests/test_auth.py | sed -n '1,220p'
fi

echo "== search authorization usage =="
rg -n "Authorization|removeprefix|bearer|Bearer|HTTPException|status_code=401" api tests -S || true

Repository: CoreyLeath-code/SentinelAI

Length of output: 589


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== api/main.py =="
awk '{printf "%6d\t%s\n", NR, $0}' api/main.py | sed -n '1,90p'

echo "== api/auth.py =="
awk '{printf "%6d\t%s\n", NR, $0}' api/auth.py | sed -n '1,160p'

echo "== tests/test_auth.py =="
awk '{printf "%6d\t%s\n", NR, $0}' tests/test_auth.py | sed -n '1,220p'

echo "== deterministic header parser behavior =="
python3 - <<'PY'
def current(authorization):
    if not authorization or not authorization.startswith("Bearer "):
        return "401"
    token = authorization.removeprefix("Bearer ").strip()
    return ("ok", token)

def proposed(authorization):
    parts = authorization.split(maxsplit=1) if authorization else []
    if len(parts) != 2 or parts[0].lower() != "bearer":
        return "401"
    token = parts[1].strip()
    return ("ok", token)

cases = [
    "Bearer test-token",
    "bearer test-token",
    "BeArEr test-token",
    "bearer  test-token",
    "Bearer",
    "",
    "Basic abc",
]
for header in cases:
    print(repr(header), "current=", current(header), "proposed=", proposed(header))
PY

Repository: CoreyLeath-code/SentinelAI

Length of output: 4641


Accept case-insensitive Bearer token schemes.

A lowercase or mixed-case scheme should not fail authentication when the token is valid. Use the proposed parser for scheme/token fields, and add a cover test such as bearer test-token in tests/test_auth.py.

🤖 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 `@api/main.py` around lines 36 - 39, Update the authorization parsing around
the Bearer validation and token extraction to accept case-insensitive scheme
values while preserving the existing token validation behavior. Use the proposed
parser for separating scheme and token fields, and add coverage in
tests/test_auth.py for a mixed- or lowercase header such as “bearer test-token”.

Comment thread api/main.py
Comment on lines 37 to +41
raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
return authorization.removeprefix("Bearer ").strip()

token = authorization.removeprefix("Bearer ").strip()
if not hmac.compare_digest(token, expected_token):
raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)api/main\.py$|(^|/)tests/test_auth\.py$|pyproject\.toml$|requirements.*\.txt$' || true

echo "== api/main.py relevant =="
if [ -f api/main.py ]; then
  cat -n api/main.py | sed -n '1,120p'
fi

echo "== tests/test_auth.py relevant =="
if [ -f tests/test_auth.py ]; then
  cat -n tests/test_auth.py | sed -n '1,220p'
fi

echo "== search auth challenge usages =="
rg -n "WWW-Authenticate|Bearer|Authorization|HTTPException|401" .

Repository: CoreyLeath-code/SentinelAI

Length of output: 5519


🌐 Web query:

FastAPI HTTPException headers WWW-Authenticate Bearer authentication challenge

💡 Result:

To include a WWW-Authenticate Bearer challenge in a FastAPI response, use the HTTPException class with the headers parameter [1][2]. When an authentication error occurs (typically a 401 Unauthorized), the HTTP specification requires that the server includes a WWW-Authenticate header to inform the client about the authentication scheme [1][3][4]. For Bearer token authentication, you should provide the following: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"},) Key points regarding this implementation: 1. Status Code: Use status.HTTP_401_UNAUTHORIZED [1][2]. Starting with FastAPI version 0.122.0, built-in security utilities (like OAuth2PasswordBearer or HTTPBearer) automatically return this 401 status code along with the appropriate WWW-Authenticate header when authentication fails [5][4]. 2. Headers Parameter: The HTTPException class accepts a headers dictionary, which allows you to pass the WWW-Authenticate instruction directly to the client [6][7]. 3. Standard Compliance: Including this header ensures your API follows HTTP specifications (such as RFC 9110), signaling to clients that they should attempt authentication using the Bearer scheme [1][3][4]. For advanced use cases or when overriding default security behavior, you can extend classes like HTTPBearer and override the make_not_authenticated_error method to customize how these 401 responses are generated [5][8].

Citations:


Include the Bearer challenge on both 401 responses.

Both _require_bearer() branches return HTTPException(status_code=401, ...) without WWW-Authenticate: Bearer. Add headers={"WWW-Authenticate": "Bearer"} to each branch, and assert the header in tests/test_auth.py.

🤖 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 `@api/main.py` around lines 37 - 41, Update both 401 HTTPException branches in
_require_bearer() to include headers={"WWW-Authenticate": "Bearer"}, covering
missing/invalid authorization and token mismatch cases. Extend
tests/test_auth.py to assert this WWW-Authenticate header on both responses.

Comment thread README.md
| `POSTGRES_DB` | `sentinel` | Postgres database |
| `OLLAMA_HOST` | `http://ollama:11434` | Ollama endpoint (optional) |
| `LLM_MODEL` | `llama2` | LLM model name |
| `API_BEARER_TOKEN` | *(unset)* | Required shared bearer token for `POST /infer`; the endpoint returns 503 until configured |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rewrite the bearer-token description.

Use Shared bearer token required for ... instead of Required shared bearer token .... This removes the awkward wording without changing the meaning.

The provided LanguageTool hint identifies this wording issue.

Proposed wording
-| `API_BEARER_TOKEN` | *(unset)* | Required shared bearer token for `POST /infer`; the endpoint returns 503 until configured |
+| `API_BEARER_TOKEN` | *(unset)* | Shared bearer token required for `POST /infer`; the endpoint returns 503 until configured |
📝 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
| `API_BEARER_TOKEN` | *(unset)* | Required shared bearer token for `POST /infer`; the endpoint returns 503 until configured |
| `API_BEARER_TOKEN` | *(unset)* | Shared bearer token required for `POST /infer`; the endpoint returns 503 until configured |
🧰 Tools
🪛 LanguageTool

[style] ~154-~154: The double modal “Required shared” is nonstandard (only accepted in certain dialects). Consider “to be shared”.
Context: ...PI_BEARER_TOKEN| *(unset)* | Required shared bearer token forPOST /infer`; the end...

(NEEDS_FIXED)

🤖 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 `@README.md` at line 154, Update the API_BEARER_TOKEN table description in
README.md to begin with “Shared bearer token required for” while preserving the
existing endpoint and 503 behavior details.

Source: Linters/SAST tools

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