From 6104791b90c1c3238e3cf60988c7ddb8b54430a3 Mon Sep 17 00:00:00 2001 From: Lev Date: Mon, 23 Mar 2026 11:11:43 +0100 Subject: [PATCH 1/3] fix(wrapper): make JWT optional in composite Bearer token (issue #53) If no colon in token, treat whole value as upstream_key; jwt_part=nil, claims={}. Requests with plain API keys (no tenant JWT) are now accepted. --- src/fairvisor/wrapper.lua | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/fairvisor/wrapper.lua b/src/fairvisor/wrapper.lua index d8f4dba..ed696a5 100644 --- a/src/fairvisor/wrapper.lua +++ b/src/fairvisor/wrapper.lua @@ -1,5 +1,6 @@ -- LLM Proxy Wrapper Mode — feature 019 --- Composite Bearer token: "Authorization: Bearer CLIENT_JWT:UPSTREAM_KEY" +-- Composite Bearer token: "Authorization: Bearer CLIENT_JWT:UPSTREAM_KEY" (JWT optional) +-- Plain Bearer token: "Authorization: Bearer UPSTREAM_KEY" (anonymous, no JWT) -- Provider registry maps path prefix → upstream host + auth scheme. -- Provider-native error bodies and streaming cutoff formats. @@ -239,9 +240,12 @@ function _M.init(deps) end -- parse_composite_bearer(auth_header) --- Returns: ok_table {jwt_part, upstream_key} or nil, reason_code +-- Returns: ok_table {jwt_part, upstream_key, claims} or nil, reason_code -- --- Composite token format: "Bearer CLIENT_JWT:UPSTREAM_KEY" +-- Composite token format: "Bearer CLIENT_JWT:UPSTREAM_KEY" (JWT present) +-- Plain token format: "Bearer UPSTREAM_KEY" (JWT absent, anonymous tenant) +-- JWT is optional. If no colon is present, the whole token is the upstream key +-- and jwt_part is nil; claims is an empty table. -- Split is at the FIRST colon in the token portion. function _M.parse_composite_bearer(auth_header) if type(auth_header) ~= "string" then @@ -257,13 +261,23 @@ function _M.parse_composite_bearer(auth_header) return nil, "composite_key_invalid" end - -- Find first colon to split JWT:KEY + -- JWT is optional. If no colon present, treat the whole token as the + -- upstream key (anonymous tenant — no JWT identity). local colon_pos = string_find(token, ":", 1, true) - if not colon_pos or colon_pos <= 1 then + if not colon_pos then + return { + jwt_part = nil, + upstream_key = token, + claims = {}, + } + end + + -- Colon present but at position 1 means empty jwt_part — invalid. + if colon_pos <= 1 then return nil, "composite_key_invalid" end - local jwt_part = string_sub(token, 1, colon_pos - 1) + local jwt_part = string_sub(token, 1, colon_pos - 1) local upstream_key = string_sub(token, colon_pos + 1) if jwt_part == "" then From 055375b87b78c7132df799abc00499f841292c4b Mon Sep 17 00:00:00 2001 From: Lev Date: Mon, 23 Mar 2026 11:12:05 +0100 Subject: [PATCH 2/3] test(wrapper): update scenarios for optional JWT in Bearer token --- spec/unit/features/wrapper.feature | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/spec/unit/features/wrapper.feature b/spec/unit/features/wrapper.feature index 49f21d6..9c95cb6 100644 --- a/spec/unit/features/wrapper.feature +++ b/spec/unit/features/wrapper.feature @@ -9,9 +9,17 @@ Feature: LLM Proxy Wrapper Mode unit behavior And the upstream_key is "sk-abc123" And JWT claims have sub "user123" - Scenario: Missing colon separator returns composite_key_invalid + Scenario: Plain upstream key without JWT succeeds Given the nginx mock is set up - And an auth header "Bearer justajwt" + And an auth header "Bearer sk-proj-abc123" + When parse_composite_bearer is called + Then parsing succeeds + And the upstream_key is "sk-proj-abc123" + And jwt_part is nil + + Scenario: Bearer with only a colon (empty JWT) returns composite_key_invalid + Given the nginx mock is set up + And an auth header "Bearer :sk-proj-abc123" When parse_composite_bearer is called Then parsing fails with reason "composite_key_invalid" @@ -178,6 +186,14 @@ Feature: LLM Proxy Wrapper Mode unit behavior Then upstream url contains "api.openai.com" And ngx exit was not called + Scenario: Plain upstream key without JWT allows request + Given the nginx mock is set up for access_handler + And request auth header is "Bearer sk-proj-plainkey" + And request path is "/openai/v1/chat/completions" + When access_handler is called + Then upstream url contains "api.openai.com" + And ngx exit was not called + Scenario: Valid composite token to unknown path returns 404 Given the nginx mock is set up for access_handler And request auth header is "Bearer eyJhbGciOiJub25lIn0.eyJzdWIiOiJ1c2VyMTIzIn0.:sk-abc123" From 123db660f3958599a424bb78064f34bf2ddd1061 Mon Sep 17 00:00:00 2001 From: Lev Date: Mon, 23 Mar 2026 11:12:33 +0100 Subject: [PATCH 3/3] test(wrapper): add jwt_part is nil step for plain-key scenarios --- spec/unit/wrapper_spec.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/unit/wrapper_spec.lua b/spec/unit/wrapper_spec.lua index b491429..b5cf70e 100644 --- a/spec/unit/wrapper_spec.lua +++ b/spec/unit/wrapper_spec.lua @@ -100,6 +100,11 @@ runner:then_("^the upstream_key is \"(.-)\"$", function(ctx, key) assert.equals(key, ctx.parsed.upstream_key) end) +runner:then_("^jwt_part is nil$", function(ctx) + assert.is_not_nil(ctx.parsed) + assert.is_nil(ctx.parsed.jwt_part) +end) + runner:then_("^JWT claims have sub \"(.-)\"$", function(ctx, sub) assert.is_not_nil(ctx.parsed) assert.is_not_nil(ctx.parsed.claims)