Repository navigation
fix(python): don't send empty-string security credentials - #130
Conversation
|
All contributors have signed the CLA ✍️ ✅ |
…nt fallback applies A security value passed as an empty string (for example `SDK(api_key_auth="")`) was treated as provided: `get_security_from_env` returned early, so the `envVarPrefix` fallback never ran, and `get_security` serialized the empty value, producing headers such as `Authorization: Bearer ` that httpx rejects as an illegal header value. Empty-string credentials are now skipped when building request security, so a later non-empty scheme can be selected, and a global security object whose credentials are all empty falls back to the environment. Basic and custom schemes keep their existing behaviour, so an explicitly empty username or password is still sent.
d6909a8 to
ef172ec
Compare
|
recheck |
1 similar comment
|
recheck |
|
@LukasParke I don't agree with the premise of this PR. Our SDKs intentionally make a distinction between an omitted credential (e.g. For example, a user wanting to disable security may explicitly set it empty: Following your PR, environment credentials would get picked up in this case, potentially authenticating a previously unauthenticated request. |
… environment An explicitly empty credential, such as SDK(api_key_auth="") or SDK(security=Security()), now stays unauthenticated even when envVarPrefix environment variables are set. get_security_from_env is back to its previous behaviour and only consults the environment when no security was passed. Empty-string credential values are still skipped when serializing, so no "Authorization: Bearer " header or empty API key header or query parameter is sent. HTTP Basic and custom schemes are unchanged.
|
@2ynn Thanks, that's a good point and I agree. An explicitly empty credential should mean "no credential", not "look in the environment". I've removed the environment fallback in 90c32e0. What's left only stops serializing empty values. Today |
2ynn
left a comment
There was a problem hiding this comment.
Today "" on a bearer/OAuth2 scheme yields Authorization: Bearer , which httpx rejects as an illegal header value, so the call errors instead of going out unauthenticated. With this change no header (or empty API key header/query parameter) is sent, which matches TypeScript's resolveSecurity/applyBearer.
I agree with that part!
Scheme selection in get_security again matches the declared security order: a field set to "" stays selected like any other non-None value instead of being skipped in favour of a later scheme. The empty value is still not serialized, so no credential is sent. Replace the selection-skipping test with one asserting that an empty first scheme stays selected and no Authorization header is sent.
…empty-credential test IDs Fold the explicit-empty-string case into test_global_security_fields_ordering instead of a separate test, and assert which Authorization header is sent. Register auth-global-security-empty and auth-global-security-flattening-empty for the empty-credential tests, which previously reused unrelated IDs. Other targets skip both IDs.
50f2665
Why
In a generated Python SDK, a security credential passed as an empty string (for example
SDK(api_key_auth="")orSecurity(access_token="")) was serialized as-is. For bearer, OAuth2 and OpenID Connect schemes this producedAuthorization: Bearer, which httpx rejects as an illegal header value (LocalProtocolError). For API keys it sent an empty header or query parameter.What changed
_parse_security_scheme_valueintemplates/templates/pythonv2/auxiliary/{{getSourcePath}}/utils/security.py.stmplreturns without writing a header or query parameter when the value is"". This covers top-level fields as well as values nested in security options and scheme models.What stays the same:
get_securitystill selects the first non-Nonefield in the declared security order, so an explicit""keeps its scheme selected and the request is sent without a credential. It does not fall back to a later scheme.""does not fall back to theenvVarPrefixenvironment variable._parse_basic_auth_schemeand custom handling, so an empty username or password is still sent as before (the existingauth-basic-auth-emptytest covers this).""is affected. Whitespace-only strings and non-string values are not changed.Generated output:
zSDKs/sdk-pythonv2/src/speakeasy/new_openapi/utils/security.py(plus itsgen.lockchecksum) is the only review SDK change. Usage snippets are not touched.Changeset:
globalSecurity/pythonv2, patch.Testing
make check-template-pythonv2,npm run format,git diff --check: pass.TARGET=primary make test-pythonv2: 672 passed. Changes:test_global_security_fields_orderingis parametrized. Thepopulatedcase is the original test and now also checks that theapi_key_authvalue is sent. Theempty-stringcase passesapi_key_auth=""withbasic_httpset:api_key_authstays selected, noAuthorizationheader is sent, and the endpoint returns 401.test_global_security_empty_ignores_env_vars(auth-global-security-empty): withSPEAKEASY_ACCESS_TOKENset, bothSecurity()andSecurity(access_token="")send noAuthorizationheader.TARGET=quaternary make test-pythonv2(flattened global security withenvVarPrefix): 248 passed, 1 skipped. Addstest_global_security_flattening_empty_string_ignores_env_var(auth-global-security-flattening-empty;SDK(api_key_auth="")withSPEAKEASY_API_KEY_AUTHset sends noAuthorizationheader).TARGET=security-options make test-pythonv2: 6 passed, 194 skipped.Authorizationheader is sent. Theomittedcase already passes onmain. It checks that the environment is not consulted when a security object is passed.TARGET=review make build-pythonv2: the review SDK diff contains only the change above.auth-global-security-emptyandauth-global-security-flattening-emptyare registered ininternal/features/tests.go(go generate ./internal/features/...). Every other target'sisTestSkippedlists them as skipped.go run ./tests -lang pythonv2reports both IDs as covered, with no unregistered IDs.make testmatrix.Public-safety check
.claude/skills/public-repo-communication/SKILL.md).git diff --check.