Skip to content

fix(sdk/go): honor gateway-configured oidc scopes in interactive flows - #3284

Open
rhuss wants to merge 3 commits into
NVIDIA:mainfrom
rhuss:fix-go-sdk-gateway-oidc-scopes
Open

fix(sdk/go): honor gateway-configured oidc scopes in interactive flows#3284
rhuss wants to merge 3 commits into
NVIDIA:mainfrom
rhuss:fix-go-sdk-gateway-oidc-scopes

Conversation

@rhuss

@rhuss rhuss commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

A gateway's metadata.json carries oidc_scopes, and the Rust CLI feeds it into every flow it starts: commands/gateway.rs reads metadata.oidc_scopes and passes it to both the interactive path and the client-credentials path.

The Go SDK reads that field in resolveClientCredentialsConfig only. Login and DeviceLogin silently ignored it and always used the built-in defaults (openid profile email), so a gateway registered with custom scopes got them for machine-to-machine auth but not for user login.

This resolves gateway scopes in both interactive flows using the same rule the client-credentials path already applies: they fill only genuinely-unset scopes, so an explicit WithScopes still wins.

Behavior change: a gateway with oidc_scopes set in metadata.json now affects Login and DeviceLogin from the Go SDK, matching what the CLI has always done.

Related Issue

No separate issue. This is a localized bug fix bringing the Go SDK in line with existing CLI behavior for a metadata field the SDK already parses, in the same spirit as #3235 (which this stacks on).

Depends on #3235. That PR adds the requireOpenIDScope() normalization this one reorders, so merge it first.

Because the branch is stacked on a fork, GitHub cannot target #3235's branch directly, so the diff below currently includes that PR's commit as well. Review only fix(sdk/go): honor gateway-configured oidc scopes in interactive flows (2dd564c); the diff resolves itself once #3235 lands.

Changes

  • oidc.go: resolveOIDCConfig resolves gwCfg.OIDCScopes into unset scopes, mirroring credentials.go. Login now normalizes openid after gateway resolution rather than before, so scopes from metadata.json are normalized too.
  • device.go: the same resolution in DeviceLogin's gateway block, with the same reordering.
  • Tests: table-driven wire-level coverage for both flows (gateway scopes applied, gateway scopes gain openid, explicit WithScopes wins, empty oidc_scopes falls back to defaults). Extracted setupScopeCapturingDeviceProvider so the device tests share one mock provider.

Testing

  • mise run go:ci green: build, golangci-lint (0 issues), gofmt, full go test -race, proto-check, docs-check.
  • mise run pre-commit green.
  • 8 new subtests across TestLogin_GatewayScopes and TestDeviceLogin_GatewayScopes.

Checklist

  • Conventional Commit message, DCO sign-off
  • Tests added and passing
  • No public API signature changed
  • Behavior change documented above

`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>
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>
A gateway's metadata.json carries oidc_scopes, and the Rust CLI feeds it
into every flow it starts (commands/gateway.rs passes
metadata.oidc_scopes to both the interactive and the client-credentials
paths). The Go SDK read the field in resolveClientCredentialsConfig
only, so Login and DeviceLogin silently ignored it and always used the
built-in defaults.

Resolve gateway scopes in both interactive flows using the same rule the
client-credentials path already applies: they fill only genuinely-unset
scopes, so an explicit WithScopes still wins. The openid normalization
now runs after gateway resolution, so scopes coming from metadata.json
are normalized too.

Behavior change: a gateway with oidc_scopes set in metadata.json now
affects Login and DeviceLogin from the Go SDK, matching the CLI.

Signed-off-by: Roland Huß <rhuss@redhat.com>
@copy-pr-bot

copy-pr-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant