fix(sdk/go): honor explicitly-set zero option values in oidc defaults - #3235
fix(sdk/go): honor explicitly-set zero option values in oidc defaults#3235rhuss wants to merge 2 commits into
Conversation
`loginConfig.applyDefaults()` keyed off the zero value rather than set-ness, so it could not distinguish an unset field from one a caller explicitly set to its zero value: - `WithTimeout(0)` was silently replaced by the 2m default. - `WithScopes()` (explicit empty) was replaced by the default scopes, even though `WithScopes` already records `scopesSet`. Switch `applyDefaults` to consult the `*Set` sentinels, add a `timeoutSet` sentinel set by `WithTimeout`, and guard the client-credentials exchange so a zero timeout means "no deadline" instead of creating an already-expired context (matching Login and DeviceFlow). `WithTimeout(0)` now means "no timeout". Unset fields still receive their defaults; non-zero explicit values are unaffected. Signed-off-by: Roland Huß <rhuss@redhat.com>
| // A zero timeout means "no deadline"; only bound the exchange when a | ||
| // positive timeout was configured (mirrors Login and DeviceFlow). | ||
| exchangeCtx := context.Background() | ||
| cancel := context.CancelFunc(func() {}) | ||
| if a.cfg.timeout > 0 { | ||
| exchangeCtx, cancel = context.WithTimeout(exchangeCtx, a.cfg.timeout) | ||
| } |
There was a problem hiding this comment.
Does context.WithTimeout() interpret a 0 value as no deadline, or does it explicitly need this handling?
There was a problem hiding this comment.
context.WithTimeout(parent, 0) doesn't mean "no deadline". It's defined as WithDeadline(parent, time.Now().Add(timeout)), so a 0 (or negative) timeout sets the deadline to now and the context is born already-expired; any call using it returns context.DeadlineExceeded immediately. So the explicit timeout > 0 guard is needed to actually get "no deadline" behavior, and it mirrors what Login and DeviceFlow already do. Without it, WithTimeout(0) would break the client-credentials exchange instead of disabling the timeout (covered by TestClientCredentialsAuthZeroTimeoutHasNoDeadline).
elezar
left a comment
There was a problem hiding this comment.
The explicit-empty-scope behavior is blocking: WithScopes() now leaves the interactive Login and DeviceLogin flows without the required openid scope. Please validate that interactive OIDC scopes contain openid (or otherwise reject/normalize an empty list), while preserving empty/non-OIDC scopes for client-credentials flows.
Two non-blocking points for clarification:
- Now that set-ness is tracked, is there a reason not to initialize a default config and then apply options? That would make option precedence explicit and may avoid sentinel-driven defaulting.
- Please define the complete
WithTimeoutcontract. The new guard treats every non-positive duration as no deadline, whereas the PR description discusses only0. Document and testd <= 0as no deadline, or reject negative values.
Login and DeviceLogin authenticate a user, so their requests must be
OpenID Connect ones. Both passed the caller's scopes through verbatim,
so WithScopes("profile") produced an authorization request without
"openid", and buildAuthURL set the parameter unconditionally, so an
explicitly-empty list emitted a bare "scope=".
Normalize the scopes for both interactive flows: "openid" is placed
first and a caller-supplied duplicate is dropped, with the remaining
scopes keeping their order. This mirrors build_scopes in
crates/openshell-cli/src/oidc_auth.rs, which the Go SDK did not follow.
The client credentials grant is left untouched. It has no user and no
ID token, so it keeps sending exactly what the caller asked for,
matching build_ci_scopes.
Also document the WithTimeout contract: the flows all gate on
timeout > 0, so any non-positive duration means "no deadline".
Signed-off-by: Roland Huß <rhuss@redhat.com>
|
Pushed 89a0f9a addressing all three points. Reasoning below. 1. Explicit empty scopes on the interactive flows (blocking)Confirmed, and there was already an answer in the repo that I should have mirrored from the start.
q.Set("scope", strings.Join(scopes, " "))An explicitly-empty list emits a literal
The real gap is broader than the empty list. The Go SDK never had the normalization the Rust CLI already does. So rather than adding validation, I ported that normalization:
This closes the empty-list case you raised and also Two supporting details that convinced me a missing
For context on why the scopes change is in this PR at all, which the description undersold: before it, Tests added: wire-level assertions that both 2. Default config, then apply optionsPartly agree, and it splits by field. For For
There is a smaller cost too: seeding needs a So the choice is one mechanism for both fields, or pre-seeding for 3.
|
Summary
oidc.loginConfig.applyDefaults()decided whether to apply a default by inspecting the field's value (len(c.scopes) == 0,c.timeout == 0) rather than whether the caller had set it. As a result, a caller who explicitly passed a zero value had it silently replaced:WithTimeout(0)became the 2-minute default.WithScopes()(explicit empty) became the default scopes, even thoughWithScopesalready recordsscopesSet.This makes
applyDefaultsconsult the*Setsentinels instead, so defaults fill only genuinely-unset fields. Unset behavior and non-zero explicit values are unchanged.Related Issue
Follow-up to a review comment on #3232 (raised by @elezar): #3232 (comment). This is a small, localized pre-existing bug fix, so no separate issue is filed.
Changes
oidc/options.go: add atimeoutSetsentinel (set byWithTimeout);applyDefaultsnow checks!scopesSet/!timeoutSetinstead of the zero value.oidc/credentials_auth.go: guard the client-credentials token exchange so a zero timeout means "no deadline" rather than an already-expired context (matching the existing guards in Login and DeviceFlow).WithTimeout(0)succeeds instead of failing on a born-expired context.Behavior change:
WithTimeout(0)now means "no timeout" instead of the 2-minute default.Testing
mise run go:cigreen (build,golangci-lint,gofmt, fullgo test, proto-check, docs-check).Checklist
WithTimeout(0)) documented above