Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changesets/1792625280-484bbfad.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
id: 1792625280-484bbfad
features:
- globalSecurity
targets:
- pythonv2
type: fix
bump: patch
description: stop sending empty-string credentials as an empty bearer token, API key header, or query parameter
author: LukasParke
date: "2026-10-21"
2 changes: 2 additions & 0 deletions internal/features/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const (
TestAuthBearerAuthOperationWithPrefix
TestAuthBearerAuthOperationWithoutPrefix
TestAuthGlobalSecurityFieldsOrdering
TestAuthGlobalSecurityEmpty
TestAuthGlobalSecurityOptionFieldsOrdering
TestAuthOauth2Auth
TestAuthOpenIDConnectAuth
Expand All @@ -58,6 +59,7 @@ const (
TestAuthGlobalSecurityFlattening
TestAuthGlobalSecurityFlatteningCallback
TestAuthGlobalSecurityFlatteningEnvVarFallback
TestAuthGlobalSecurityFlatteningEmpty
TestAuthHoistedOperationAuthRetained
TestAuthCustomSecurityOptionAppId
TestAuthCustomSecuritySchemeAppId
Expand Down
10 changes: 10 additions & 0 deletions internal/features/tests_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions templates/templates/cli/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,8 @@ function isTestSkipped(test: string): boolean {
"parameters-path-parameter-formats",
"shared-enum-constructor-default",
"open-union-invalid-json-is-an-error",
"auth-global-security-empty",
"auth-global-security-flattening-empty",
].includes(test);
}

Expand Down
2 changes: 2 additions & 0 deletions templates/templates/csharp/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@ function isTestSkipped(test: string): boolean {
"event-stream-absent-required-union",
"event-stream-equivalent-number-lexemes",
"event-stream-null-and-undeclared-fields",
"auth-global-security-empty",
"auth-global-security-flattening-empty",
].includes(test);
}

Expand Down
2 changes: 2 additions & 0 deletions templates/templates/go/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@ function isTestSkipped(test: string): boolean {
"event-stream-absent-required-union",
"event-stream-equivalent-number-lexemes",
"event-stream-null-and-undeclared-fields",
"auth-global-security-empty",
"auth-global-security-flattening-empty",
].includes(test);
}

Expand Down
2 changes: 2 additions & 0 deletions templates/templates/javav2/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,8 @@ function isTestSkipped(test: string): boolean {
"event-stream-absent-required-union",
"event-stream-equivalent-number-lexemes",
"event-stream-null-and-undeclared-fields",
"auth-global-security-empty",
"auth-global-security-flattening-empty",
].includes(test);
}

Expand Down
2 changes: 2 additions & 0 deletions templates/templates/mcp-typescript/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,8 @@ function isTestSkipped(test: string): boolean {
"event-stream-absent-required-union",
"event-stream-equivalent-number-lexemes",
"event-stream-null-and-undeclared-fields",
"auth-global-security-empty",
"auth-global-security-flattening-empty",
].includes(test);
}

Expand Down
2 changes: 2 additions & 0 deletions templates/templates/php/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,8 @@ function isTestSkipped(test: string): boolean {
"event-stream-absent-required-union",
"event-stream-equivalent-number-lexemes",
"event-stream-null-and-undeclared-fields",
"auth-global-security-empty",
"auth-global-security-flattening-empty",
].includes(test);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ def _parse_security_scheme_value(
scheme_type = scheme_metadata.scheme_type
sub_type = scheme_metadata.sub_type

if value == "":
return

header_name = security_metadata.get_field_name(field_name)

if scheme_type == "apiKey":
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import os

import httpx
import pytest

from openapi.models import shared
Expand All @@ -23,21 +26,60 @@ def test_function_callbacks_for_o_auth_support_global_security():
assert res.token.token == "global"


def test_global_security_fields_ordering():
@pytest.mark.parametrize(
"api_key_auth,expected_authorization",
[("testApiKey", "testApiKey"), ("", None)],
ids=["populated", "empty-string"],
)
def test_global_security_fields_ordering(api_key_auth, expected_authorization):
record_test("auth-global-security-fields-ordering")

captured = {}

def capture(request):
captured["headers"] = request.headers

# When maintainOpenApiOrder: false is set, the first non-nil field (in alphabetical order) is selected.
# In this case api_key_auth takes precedence over basic_http, which is invalid for this endpoint.
# An explicitly empty api_key_auth stays selected and sends no credential.
s = SDK(
security=shared.Security(
api_key_auth="testApiKey",
api_key_auth=api_key_auth,
basic_http=shared.SchemeBasicHTTP(username="testUser", password="testPass"),
),
client=httpx.Client(event_hooks={"request": [capture]}),
)

with pytest.raises(APIError) as exc_info:
s.auth.global_security_basic_http()
assert exc_info.value.status_code == 401
assert captured["headers"].get("authorization") == expected_authorization


@pytest.mark.parametrize(
"security",
[shared.Security(), shared.Security(access_token="")],
ids=["omitted", "empty-string"],
)
def test_global_security_empty_ignores_env_vars(security):
record_test("auth-global-security-empty")

captured = {}

def capture(request):
captured["headers"] = request.headers
return httpx.Response(401, request=request)

os.environ["SPEAKEASY_ACCESS_TOKEN"] = "global"
try:
client = httpx.Client(transport=httpx.MockTransport(capture))
s = SDK(server_url=HTTPBIN_URL, security=security, client=client)
with pytest.raises(APIError):
s.auth.global_bearer_auth()
finally:
os.environ.pop("SPEAKEASY_ACCESS_TOKEN", None)

assert "authorization" not in captured["headers"]


def test_hoisted_security_access_token_only():
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
import httpx
from openapi import SDK, AsyncSDK
from openapi.models.errors import SDKError
from .common_helpers import *
import pytest

Expand Down Expand Up @@ -59,3 +61,24 @@ async def test_global_security_flattening_env_var_fallback_async():
assert res.status_code == 200
finally:
os.environ.pop("SPEAKEASY_API_KEY_AUTH", None)


def test_global_security_flattening_empty_string_ignores_env_var():
record_test("auth-global-security-flattening-empty")

captured = {}

def capture(request):
captured["headers"] = request.headers
return httpx.Response(401, request=request)

os.environ["SPEAKEASY_API_KEY_AUTH"] = "Bearer testToken"
try:
client = httpx.Client(transport=httpx.MockTransport(capture))
with SDK(server_url=HTTPBIN_URL, api_key_auth="", client=client) as s:
with pytest.raises(SDKError):
s.auth.api_key_auth_global()
finally:
os.environ.pop("SPEAKEASY_API_KEY_AUTH", None)

assert "authorization" not in captured["headers"]
2 changes: 2 additions & 0 deletions templates/templates/ruby/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,8 @@ function isTestSkipped(test: string): boolean {
"event-stream-absent-required-union",
"event-stream-equivalent-number-lexemes",
"event-stream-null-and-undeclared-fields",
"auth-global-security-empty",
"auth-global-security-flattening-empty",
].includes(test);
}

Expand Down
2 changes: 2 additions & 0 deletions templates/templates/typescriptv2/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@ function isTestSkipped(test: string): boolean {
"event-stream-absent-required-union",
"event-stream-equivalent-number-lexemes",
"event-stream-null-and-undeclared-fields",
"auth-global-security-empty",
"auth-global-security-flattening-empty",
].includes(test);
}

Expand Down
2 changes: 2 additions & 0 deletions templates/templates/unity/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,8 @@ function isTestSkipped(test: string): boolean {
"event-stream-absent-required-union",
"event-stream-equivalent-number-lexemes",
"event-stream-null-and-undeclared-fields",
"auth-global-security-empty",
"auth-global-security-flattening-empty",
].includes(test);
}

Expand Down
2 changes: 1 addition & 1 deletion zSDKs/sdk-pythonv2/.speakeasy/gen.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ def _parse_security_scheme_value(
scheme_type = scheme_metadata.scheme_type
sub_type = scheme_metadata.sub_type

if value == "":
return

header_name = security_metadata.get_field_name(field_name)

if scheme_type == "apiKey":
Expand Down
Loading