Skip to content

fix(auth): accept a raw token on the Otari-Key header, not just Bearer#323

Merged
njbrake merged 3 commits into
mainfrom
fix/otari-key-raw-token
Jul 21, 2026
Merged

fix(auth): accept a raw token on the Otari-Key header, not just Bearer#323
njbrake merged 3 commits into
mainfrom
fix/otari-key-raw-token

Conversation

@njbrake

@njbrake njbrake commented Jul 20, 2026

Copy link
Copy Markdown
Member

Description

The dashboard's copy-paste curl snippet sends -H "Otari-Key: gw-..." (no Bearer prefix), but the gateway required Bearer on that header and rejected it with 401 {"detail":"Invalid header format. Expected 'Bearer <token>'"}. Requiring the Bearer scheme inside a header literally named Otari-Key is also surprising.

This makes the branded key headers (the canonical Otari-Key, and the legacy AnyLLM-Key / X-AnyLLM-Key aliases) carry the token directly: a Bearer prefix is accepted and stripped for back-compat, but is no longer required. It matches the raw x-api-key path already supported for Anthropic-native clients (added in #315). The standard Authorization header still requires the Bearer scheme, and header precedence is unchanged.

Net effect: Otari-Key: gw-... now authenticates (what the dashboard hands out), and Otari-Key: Bearer gw-... keeps working.

PR Type

  • Bug Fix

Relevant issues

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck; the extract-token unit tests and a new end-to-end raw-header integration test; integration runs isolated on SQLite in the sandbox, so Postgres CI is the real signal).
  • Documentation was updated where necessary (no docs affected).
  • If the API contract changed, I regenerated the OpenAPI spec (no schema change; auth header parsing only).

AI Usage

  • This is fully AI-generated.

AI Model/Tool used: Claude Code (Opus 4.8, 1M context)

Any additional AI details you'd like to share: Diagnosed from @njbrake hitting the 401 with the dashboard's own snippet against a live gateway; he chose the fix direction (make the branded header accept a raw token rather than change the snippet to require Bearer). Reasoning and decision are his; the code and prose are the agent's.

  • I am an AI Agent filling out this form (check box if true)

🤖 Generated with Claude Code

Summary

  • Updated branded and legacy API key headers to accept raw tokens, while continuing to support Bearer-prefixed values.
  • Kept Authorization validation and header precedence unchanged.
  • Added unit and integration coverage for raw-token authentication.

This aligns authentication with the dashboard’s generated curl commands while preserving backward compatibility.

@njbrake
njbrake temporarily deployed to integration-tests July 20, 2026 16:33 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@njbrake, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 080277df-2fc0-4647-8890-73ca2c93795d

📥 Commits

Reviewing files that changed from the base of the PR and between 7890b44 and 96f0825.

📒 Files selected for processing (6)
  • docs/api-reference.md
  • docs/files.md
  • docs/quickstart.md
  • src/gateway/api/deps.py
  • tests/integration/test_key_management.py
  • tests/unit/test_extract_bearer_token.py

Walkthrough

Branded API key headers now accept raw tokens and optional Bearer prefixes. Unit tests cover canonical and legacy headers, while integration coverage verifies both forms against the models endpoint.

Changes

API key header authentication

Layer / File(s) Summary
Branded header token extraction
src/gateway/api/deps.py
_extract_bearer_token returns the first present canonical or legacy branded header value, stripping an optional Bearer prefix.
Authentication behavior validation
tests/unit/test_extract_bearer_token.py, tests/integration/test_key_management.py
Unit and integration tests verify raw-token and Bearer-prefixed authentication for branded API key headers.

Estimated code review effort: 3 (Moderate) | ~15 minutes

Possibly related PRs

Suggested reviewers: tbille

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific, imperative, and clearly matches the auth-header behavior change.
Description check ✅ Passed The description follows the template and includes the change summary, type, checklist, and AI usage.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/otari-key-raw-token
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/otari-key-raw-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.

@coderabbitai
coderabbitai Bot requested a review from tbille July 20, 2026 16:34

@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.

🧹 Nitpick comments (1)
src/gateway/api/deps.py (1)

77-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using str.removeprefix().

Since this codebase uses Python 3.10+ (as indicated by the | union type syntax in other functions), you can optionally simplify the prefix stripping by utilizing the built-in removeprefix method. It's a clean, pythonic way to replace the .startswith() condition and string slicing.

💡 Proposed refactor
-    for header in (API_KEY_HEADER, *LEGACY_API_KEY_HEADERS):
-        value = request.headers.get(header)
-        if value:
-            return value[7:] if value.startswith("Bearer ") else value
+    for header in (API_KEY_HEADER, *LEGACY_API_KEY_HEADERS):
+        value = request.headers.get(header)
+        if value:
+            return value.removeprefix("Bearer ")
🤖 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 `@src/gateway/api/deps.py` around lines 77 - 92, Update the prefix handling in
the API key extraction loop of the request-header function to use Python’s
str.removeprefix() for removing "Bearer ". Preserve the existing behavior for
raw tokens and legacy headers.
🤖 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.

Nitpick comments:
In `@src/gateway/api/deps.py`:
- Around line 77-92: Update the prefix handling in the API key extraction loop
of the request-header function to use Python’s str.removeprefix() for removing
"Bearer ". Preserve the existing behavior for raw tokens and legacy headers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c5f0fbdb-5850-4779-a059-a781583296e6

📥 Commits

Reviewing files that changed from the base of the PR and between 4fe10c1 and 7890b44.

📒 Files selected for processing (3)
  • src/gateway/api/deps.py
  • tests/integration/test_key_management.py
  • tests/unit/test_extract_bearer_token.py

@khaledosman

Copy link
Copy Markdown
Member

Reviewed — LGTM, no blocking issues.

Checked correctness, the security posture, tests, and docs:

  • Precedence preserved. The new (Otari-Key, *legacy)Authorizationx-api-key order matches the old flow, and Authorization still enforces the Bearer scheme. The precedence tests still guard it.
  • No auth regression. Accepting a raw token only broadens the accepted input format; validation still hinges on the SHA-256 hash lookup, so garbage on a branded header now falls through to the same 401 (via hash_key) instead of the format 401. Nothing is left unguarded, and no credential leaks.
  • Edge cases hold. Empty/absent headers fall through as before; a Bearer -only value degrades to the same empty-token behavior the Authorization path already had, and gw--prefixed keys never mis-strip.
  • Docs fully swept. Grepped docs/, README.md, and web/ — no stale Otari-Key: Bearer snippets left behind.
  • Coverage is right. The removed malformed-header unit tests were correctly dropped (those inputs are now valid raw tokens), and the new integration test exercises the exact dashboard curl shape plus the Bearer back-compat form end-to-end.

Optional nit (non-blocking): the helper is still named _extract_bearer_token though it no longer requires Bearer on the branded headers. It's private and renaming touches three call sites, so leaving it is fine.

Review created by Claude Code.

Copilot AI 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.

Pull request overview

Aligns gateway authentication with the dashboard’s generated curl snippet by allowing the branded API key headers (Otari-Key and legacy aliases) to carry a raw token directly, while preserving backward compatibility for Bearer <token> on those headers and keeping Authorization: Bearer <token> behavior unchanged.

Changes:

  • Updated _extract_bearer_token to accept raw tokens on Otari-Key / legacy key headers (optionally stripping a leading Bearer ).
  • Added/updated unit and integration tests to cover raw-header authentication and back-compat Bearer form.
  • Updated docs examples to show Otari-Key: <token> (no Bearer prefix required).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/gateway/api/deps.py Adjusts header parsing so branded key headers accept raw tokens (Bearer prefix optional).
tests/unit/test_extract_bearer_token.py Adds unit coverage for raw-token extraction on canonical/legacy key headers.
tests/integration/test_key_management.py Adds end-to-end test proving Otari-Key: <token> authenticates, and Bearer form remains supported.
docs/quickstart.md Updates curl examples to use raw Otari-Key: <token>.
docs/files.md Updates file upload and chat examples to use raw Otari-Key: <token>.
docs/api-reference.md Updates auth header guidance to prefer raw Otari-Key: <token> while noting Bearer back-compat.

Comment thread src/gateway/api/deps.py Outdated
Comment thread docs/api-reference.md Outdated
@njbrake
njbrake force-pushed the fix/otari-key-raw-token branch from 02522cc to 922ebba Compare July 21, 2026 10:30
@njbrake
njbrake temporarily deployed to integration-tests July 21, 2026 10:31 — with GitHub Actions Inactive
@njbrake
njbrake temporarily deployed to integration-tests July 21, 2026 10:34 — with GitHub Actions Inactive
njbrake and others added 3 commits July 21, 2026 10:35
The branded key headers (Otari-Key and the legacy AnyLLM-Key / X-AnyLLM-Key
aliases) now carry the token directly. A "Bearer " prefix is still accepted
and stripped for back-compat, but is no longer required, so the dashboard's
copy-paste snippet (Otari-Key: gw-...) authenticates. This matches the raw
x-api-key convention already supported for Anthropic-native clients. The
standard Authorization header still requires the Bearer scheme.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gateway now accepts a raw token on the branded Otari-Key header (a
Bearer prefix is still accepted for back-compat), so drop the surprising
Bearer prefix from the docs examples to match the header's naming and the
snippet the dashboard hands out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A trailing space inside an inline-code span renders inconsistently across
Markdown renderers; the prefix reads fine without it.
@njbrake
njbrake force-pushed the fix/otari-key-raw-token branch from f8c296d to 96f0825 Compare July 21, 2026 10:35
@njbrake
njbrake temporarily deployed to integration-tests July 21, 2026 10:35 — with GitHub Actions Inactive
@njbrake
njbrake merged commit 794eeef into main Jul 21, 2026
5 checks passed
@njbrake
njbrake deleted the fix/otari-key-raw-token branch July 21, 2026 10:38
@njbrake njbrake mentioned this pull request Jul 22, 2026
4 tasks
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.

3 participants