Skip to content

feat(ENG-13680): add a NuGet credential provider - #494

Open
cloudsmith-iduffy wants to merge 1 commit into
iduffy/shared-credential-helper-codefrom
iduffy/nuget-credential-provider
Open

cloudsmith-iduffy wants to merge 1 commit into
iduffy/shared-credential-helper-codefrom
iduffy/nuget-credential-provider

Conversation

@cloudsmith-iduffy

@cloudsmith-iduffy cloudsmith-iduffy commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Adds a NuGet credential provider (ENG-13680). After a one-off install, dotnet restore, MSBuild and Visual Studio authenticate to Cloudsmith feeds with the CLI's own credentials: an API key, a SAML session from the keyring, or OIDC. No token goes into nuget.config.

$ cloudsmith credential-helper install nuget --workspace acme
$ dotnet restore

Install. credential-helper install nuget writes a nuget-plugin-cloudsmith launcher next to cloudsmith, as a .bat on Windows. NuGet 6.13 and later (.NET SDK 9.0.200+) finds any nuget-plugin-* executable on PATH, so no NuGet config changes.

NuGet often runs the plugin from an IDE or a build agent that does not have your shell's environment. So the launcher keeps the CLOUDSMITH_* settings that you gave at install time as a flag or an environment variable: the Workspace, profile, config and credentials files, API host, TLS verification, OIDC and keyring settings. These values win over the runtime environment (ENG-15016). Secrets and the proxy URL are never written, because the launcher is world-readable. On Windows, the installer refuses a value that a batch file cannot hold safely.

Runtime. credential-helper nuget speaks NuGet's cross-platform plugin protocol. NuGet asks every plugin about every feed:

  • An https feed on *.cloudsmith.io, *.cloudsmith.com or a NuGet domain of the Workspace → Basic token:<credential>.
  • Any other feed, or a failed domain lookup → NotFound, so NuGet tries its other credential sources.
  • A Cloudsmith feed with no credential → Error, with a message that says how to authenticate.
  • A retry after a 401 resolves the credential again, so a renewed SAML or OIDC token is used.

--domain <host> adds hosts that always match. --no-discover turns off the domain lookup, so only those hosts and the Cloudsmith hosts match. Stdout carries only protocol messages, also under --debug.

Changes outside NuGet.

  • core/api/init.py writes its --debug credential messages to stderr. One of them now says "SAML" instead of "SSO".
  • On Windows, launcher_arg() checks the standalone binary path with batch_value. This also applies to the docker, pnpm, cargo and terraform .cmd launchers.
  • resolve_credentials keeps its CredentialContext on opts for the retry. is_cloudsmith_domain passes configure_api through, so the domain lookup keeps the CLI's proxy and TLS settings.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • Other (please describe)

Additional Notes

This replaces #458, which has the earlier review history.

Testing. A temporary workflow ran a real dotnet restore (SDK 10) on Linux, macOS and Windows, from both a pip install and the PyInstaller binary. It covered a standard feed, a custom domain found by the live lookup with --debug on, and --domain --no-discover with an unreachable API host. Each job checks that the restore fails with 401 without the plugin and succeeds with it. All 18 jobs passed: run 38967385354.

I also ran dotnet nuget push and dotnet restore locally against a private repo with only a SAML session. Both worked with no API key.

Known limits.

NuGet's plugin timeouts are 30 seconds in ProtocolConstants, not the 5 seconds in the Microsoft docs. Credentials resolve before the handshake, so the plugin needs no threads or progress messages.

🤖 Generated with Claude Code

`cloudsmith credential-helper install nuget` writes a
`nuget-plugin-cloudsmith` launcher (`.bat` on Windows) on PATH. NuGet 6.13+
finds it and speaks the cross-platform plugin protocol to
`cloudsmith credential-helper nuget`. The plugin answers for Cloudsmith hosts
and the NuGet domains of the Workspace that is written into the launcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 21:36
@cloudsmith-iduffy
cloudsmith-iduffy requested a review from a team as a code owner October 16, 2026 21:36
@cloudsmith-iduffy
cloudsmith-iduffy added this pull request to stack #495 October 16, 2026 21:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Blocking credential operations can exceed NuGet protocol deadlines, and status detection can miss default installations.

3 open issues
What changed in this PR

Adds a NuGet credential provider that authenticates Cloudsmith feeds using the CLI’s existing credentials.

Changes:

  • Implements the NuGet plugin protocol and feed matching.
  • Adds installation, registration, and saved launcher settings.
  • Adds tests and protects protocol output from debug messages.
File Description
cloudsmith_cli/​credential_helpers/​nuget/​runtime.py Implements plugin messages and authentication.
cloudsmith_cli/​credential_helpers/​nuget/​installer.py Manages NuGet launcher installation.
cloudsmith_cli/​credential_helpers/​nuget/​__init__.py Exports the runtime entry point.
cloudsmith_cli/​credential_helpers/​launchers.py Supports saved settings and Windows batch launchers.
cloudsmith_cli/​credential_helpers/​common.py Allows discovery using existing API configuration.
cloudsmith_cli/​core/​api/​init.py Sends authentication debug output to stderr.
cloudsmith_cli/​cli/​tests/​test_credential_helper_nuget.py Tests protocol, command, and installation behavior.
cloudsmith_cli/​cli/​tests/​commands/​test_credential_helper_install.py Tests launcher settings and escaping.
cloudsmith_cli/​cli/​decorators.py Exposes credential context for renewal.
cloudsmith_cli/​cli/​commands/​credential_helper/​nuget.py Adds the NuGet command.
cloudsmith_cli/​cli/​commands/​credential_helper/​manage.py Registers installation and captures settings.
cloudsmith_cli/​cli/​commands/​credential_helper/​__init__.py Registers the NuGet subcommand.
CHANGELOG.md Documents provider usage and requirements.

Review effort: Balanced

)
@click.argument("nuget_args", nargs=-1, type=click.UNPROCESSED)
@common_api_auth_options
@initialise_api
Comment on lines +141 to +143
launcher_path = self._launcher_path(resolve_bin_dir())
if not launcher_path.exists():
return {"launcher": None, "hosts": []}
if method == "Close":
return False

response = self.handle_request(method, message.get("Payload"))

This branch has not been deployed

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants