Repository navigation
fix(python): don't send empty-string security credentials - #130
LukasParke wants to merge 4 commits into
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!
| s = SDK( | ||
| server_url=HTTPBIN_URL, | ||
| security=shared.Security(access_token="", api_key_auth="", oauth2="global"), | ||
| ) | ||
|
|
||
| res = s.auth.global_bearer_auth() | ||
| assert res.http_meta is not None | ||
| assert res.http_meta.response is not None | ||
| assert res.http_meta.response.status_code == 200 | ||
| assert res.token is not None | ||
| assert res.token.token == "global" |
There was a problem hiding this comment.
I still disagree with skipping explicitly empty credentials during scheme selection.
Under maintainOpenAPIOrder: true (default) our SDKs preserves the security options’ declared order:
security:
- apiKeyAuth: []
- oauth2: []
- clientCredentials: [read, write]
- customSchemeAppId: []
- basicHttp: []
- accessToken: []
- {}
In this example, apiKeyAuth comes first so if the caller explicitly passes api_key_auth="", that option should remain selected.
There was a problem hiding this comment.
Agreed, thanks. I removed the selection change in ccf608a, so get_security matches main again. An explicit "" keeps its declared scheme selected and the request simply carries no credential; it does not fall back to a later scheme. I replaced the skipping test with test_global_security_fields_ordering_keeps_empty_string_selected, which passes Security(access_token="", oauth2="global") and asserts no Authorization header is sent.
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.
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. Adds:test_global_security_fields_ordering_keeps_empty_string_selected:Security(access_token="", oauth2="global")keeps the first field selected and sends noAuthorizationheader.test_global_security_empty_ignores_env_vars: 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(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-fields-orderingandauth-global-security-flattening-env-var-fallbacktest IDs instead of adding new IDs, so other targets' skip lists don't need to change.make testmatrix.Public-safety check
.claude/skills/public-repo-communication/SKILL.md).git diff --check.Summary by cubic
Fixes generated Python SDKs so empty-string security credentials are never serialized as an empty bearer token, API key header, or query parameter, which httpx rejects.
""counts as unset; whitespace-only values, non-string values, and HTTP Basic/custom schemes keep existing behavior.Written for commit 90c32e0. Summary will update on new commits.