Skip to content

feat: Add OIDC authentication with automatic CI/CD platform detection#267

Open
cloudsmith-iduffy wants to merge 1 commit intomasterfrom
iduffy/automatic-oidc
Open

feat: Add OIDC authentication with automatic CI/CD platform detection#267
cloudsmith-iduffy wants to merge 1 commit intomasterfrom
iduffy/automatic-oidc

Conversation

@cloudsmith-iduffy
Copy link
Contributor

Implements AWS-style credential provider chain with automatic OIDC token discovery for 7 major CI/CD platforms (GitHub Actions, GitLab CI, CircleCI, Azure DevOps, Bitbucket Pipelines, AWS, and Jenkins/Generic).

Type of Change:

  • New feature

Key Features:

  • Credential provider chain with priority: CLI flag > env var > config file > keyring > OIDC
  • Auto-detects CI/CD environment and exchanges vendor JWT for Cloudsmith token
  • Keyring-first token caching with disk fallback
  • New cloudsmith print-token command to export tokens for curl, docker, etc.
  • Platform-specific OIDC detection shown in whoami --verbose output

Platform Support:

  • GitHub Actions: Fetches token via ACTIONS_ID_TOKEN_REQUEST_URL
  • GitLab CI: Reads CI_JOB_JWT_V2 or CI_JOB_JWT
  • CircleCI: Reads CIRCLE_OIDC_TOKEN_V2 or CIRCLE_OIDC_TOKEN
  • Azure DevOps: Fetches token via SYSTEM_OIDCREQUESTURI
  • Bitbucket Pipelines: Reads BITBUCKET_STEP_OIDC_TOKEN
  • AWS (ECS/EKS/EC2/Lambda): Uses boto3 credential chain + STS GetWebIdentityToken
  • Generic/Jenkins: Reads CLOUDSMITH_OIDC_TOKEN (requires credentials-binding plugin)

Example Usage (AWS):

$ env | grep CLOUDSMITH_
CLOUDSMITH_ORG=iduffy-demo
CLOUDSMITH_SERVICE_SLUG=default-v9ty

$ stat ~/.cloudsmith/config.ini
stat: cannot stat '/Users/iduffy/.cloudsmith/config.ini': No such file or directory

$ aws sts get-caller-identity
{
    "UserId": "AROA47EXAMPLE:ian@ianduffy.ie",
    "Account": "893EXAMPLE",
    "Arn": "arn:aws:sts::893EXAMPLE:assumed-role/AWSReservedSSO_NOPE_..."
}

$ cloudsmith whoami --verbose
Retrieving your authentication status from the API ... OK

User: default (slug: default-v9ty)

Authentication Method: OIDC Auto-Discovery
  Source: OIDC auto-discovery: AWS (org: iduffy-demo)
  Token Slug: sACcPOv3Iro8
  Created: 2025-06-07T19:43:47.840466Z

💡 Export this token: cloudsmith print-token

$ cloudsmith list repos

$ cloudsmith print-token
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...

$ curl -H "X-Api-Key: $(cloudsmith print-token)" https://api.cloudsmith.io/v1/user/self/

$ docker login docker.cloudsmith.io -u token -p $(cloudsmith print-token)

Configuration:

Requires two environment variables:

  • CLOUDSMITH_ORG: Organization slug
  • CLOUDSMITH_SERVICE_SLUG: Service account slug

Optional:

  • CLOUDSMITH_OIDC_AUDIENCE: Override default audience (default: "cloudsmith")
  • CLOUDSMITH_NO_KEYRING=1: Skip keyring, use disk cache only

Installation:

  • Base install: pip install cloudsmith-cli
  • With AWS support: pip install cloudsmith-cli[aws]
  • All features: pip install cloudsmith-cli[all]

Security:

  • Tokens cached with 60-second expiry margin for auto-refresh
  • Cache files ideally use keyring, but if they go to disk created with 0o600 permissions
  • Exponential backoff with jitter for retry resilience

Backwards Compatibility:

  • No breaking changes to existing authentication methods
  • OIDC is opt-in via environment variables
  • Existing API key/SSO authentication unaffected

Copilot AI review requested due to automatic review settings February 28, 2026 13:39
@cloudsmith-iduffy cloudsmith-iduffy requested a review from a team as a code owner February 28, 2026 13:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an OIDC-based authentication fallback to the Cloudsmith CLI (with CI/CD platform auto-detection) plus a new cloudsmith print-token helper command, aiming to support seamless auth in common pipeline environments.

Changes:

  • Introduces OIDC detectors (GitHub Actions, GitLab CI, CircleCI, Azure DevOps, Bitbucket Pipelines, AWS, Generic) and an exchange flow to obtain a Cloudsmith token.
  • Adds token caching (keyring-first with disk fallback) and keyring support for OIDC token storage.
  • Adds cloudsmith print-token and updates whoami --verbose / docs to surface OIDC and token-export guidance.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
setup.py Adds extras_require for optional AWS dependencies.
cloudsmith_cli/core/tests/test_credentials.py Comprehensive tests for provider chain, detectors, exchange, and caching.
cloudsmith_cli/core/keyring.py Adds keyring storage helpers for OIDC token payloads.
cloudsmith_cli/core/credentials/init.py Introduces credential context/result abstractions and provider chain.
cloudsmith_cli/core/credentials/providers.py Adds env/config/keyring/OIDC credential providers.
cloudsmith_cli/core/credentials/oidc/init.py OIDC package module docs.
cloudsmith_cli/core/credentials/oidc/cache.py Implements keyring-first cache with disk fallback and expiry handling.
cloudsmith_cli/core/credentials/oidc/exchange.py Implements /openid/{org}/ exchange with retries/backoff.
cloudsmith_cli/core/credentials/oidc/detectors/init.py Detector registry + priority-ordered detection.
cloudsmith_cli/core/credentials/oidc/detectors/base.py Base detector API + audience helper.
cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py GitHub Actions token retrieval via runtime endpoint.
cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py GitLab JWT env-var detection/selection.
cloudsmith_cli/core/credentials/oidc/detectors/circleci.py CircleCI JWT env-var detection/selection.
cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py Azure DevOps token retrieval via request URI.
cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py Bitbucket token env-var detector.
cloudsmith_cli/core/credentials/oidc/detectors/aws.py AWS identity-based token retrieval via STS (optional boto3).
cloudsmith_cli/core/credentials/oidc/detectors/generic.py Generic/Jenkins env-var fallback detector.
cloudsmith_cli/core/api/init.py Hooks OIDC as last-resort when no SSO/API key present.
cloudsmith_cli/cli/commands/whoami.py Adds OIDC source reporting and print-token hint in verbose output.
cloudsmith_cli/cli/commands/print_token.py New print-token command to output active token.
cloudsmith_cli/cli/commands/login.py Adds a hint about using print-token after login.
cloudsmith_cli/cli/commands/init.py Registers print-token command module import.
README.md Documents optional extras and AWS OIDC installation guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +3
"""Bitbucket Pipelines OIDC detector.
Reads OIDC token from the ``BITBUCKET_STEP_OIDC_TOKEN`` environment variable
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

New source files in this repo appear to include a copyright header comment near the top (e.g., cloudsmith_cli/cli/commands/logout.py:1). Consider adding # Copyright 2026 Cloudsmith Ltd above the module docstring here to match that convention.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
"""OIDC token cache.
Caches Cloudsmith API tokens obtained via OIDC exchange to avoid unnecessary
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

New source files in this repo appear to include a copyright header comment near the top (e.g., cloudsmith_cli/cli/commands/logout.py:1). Consider adding # Copyright 2026 Cloudsmith Ltd above the module docstring here to match that convention.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
"""Credential Provider Chain for Cloudsmith CLI.

Implements an AWS SDK-style credential resolution chain that evaluates
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

New source files in this repo appear to include a copyright header comment near the top (e.g., cloudsmith_cli/cli/commands/logout.py:1). Consider adding # Copyright 2026 Cloudsmith Ltd above the module docstring here to match that convention.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
"""AWS OIDC detector.
Uses boto3 to auto-discover AWS credentials (via any mechanism: env vars,
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

New source files in this repo appear to include a copyright header comment near the top (e.g., cloudsmith_cli/cli/commands/logout.py:1). Consider adding # Copyright 2026 Cloudsmith Ltd above the module docstring here to match that convention.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
"""Azure DevOps OIDC detector.
Fetches OIDC token via the ``SYSTEM_OIDCREQUESTURI`` HTTP endpoint using
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

New source files in this repo appear to include a copyright header comment near the top (e.g., cloudsmith_cli/cli/commands/logout.py:1). Consider adding # Copyright 2026 Cloudsmith Ltd above the module docstring here to match that convention.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
"""CircleCI OIDC detector.
Reads OIDC token from the ``CIRCLE_OIDC_TOKEN_V2`` or ``CIRCLE_OIDC_TOKEN``
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

New source files in this repo appear to include a copyright header comment near the top (e.g., cloudsmith_cli/cli/commands/logout.py:1). Consider adding # Copyright 2026 Cloudsmith Ltd above the module docstring here to match that convention.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
"""GitHub Actions OIDC detector.
Fetches OIDC token via the Actions runtime HTTP endpoint.
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

New source files in this repo appear to include a copyright header comment near the top (e.g., cloudsmith_cli/cli/commands/logout.py:1). Consider adding # Copyright 2026 Cloudsmith Ltd above the module docstring here to match that convention.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
"""CLI/Commands - Print the active authentication token."""

import click
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

New source files in this repo appear to include a copyright header comment near the top (e.g., cloudsmith_cli/cli/commands/logout.py:1). Consider adding # Copyright 2026 Cloudsmith Ltd above the module docstring here to match that convention.

Copilot uses AI. Check for mistakes.
Comment on lines 165 to 199
# Detect CI/CD environment and get vendor JWT
detector = detect_environment(debug=context.debug)
if detector is None:
if context.debug:
logger.debug("OidcProvider: No CI/CD environment detected, skipping")
return None

try:
vendor_token = detector.get_token()
except Exception: # pylint: disable=broad-exception-caught
# Detector token retrieval can fail in various ways
logger.debug(
"OidcProvider: Failed to retrieve OIDC token from %s",
detector.name,
exc_info=True,
)
return None

if not vendor_token:
logger.debug(
"OidcProvider: %s detector returned empty token", detector.name
)
return None

# Check cache before doing a full exchange
from .oidc.cache import get_cached_token, store_cached_token

cached = get_cached_token(context.api_host, org, service_slug)
if cached:
logger.debug("OidcProvider: Using cached OIDC token")
return CredentialResult(
api_key=cached,
source_name="oidc",
source_detail=f"OIDC via {detector.name} [cached] (org: {org}, service: {service_slug})",
)
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

OidcProvider.resolve() calls detector.get_token() before checking the cached Cloudsmith token. This means even a cache hit still triggers platform-specific token retrieval (e.g., extra HTTP call in GitHub/Azure) and can fail even though a valid cached token exists. Consider checking get_cached_token(...) immediately after environment detection (before get_token()), and only fetching the vendor JWT when the cache is empty/expired.

Copilot uses AI. Check for mistakes.
Comment on lines 18 to 40
def _try_oidc_credential(config):
"""Attempt OIDC auto-discovery as a last-resort credential provider.

Only activates when CLOUDSMITH_ORG and CLOUDSMITH_SERVICE_SLUG are set.
"""
from ..credentials import CredentialContext
from ..credentials.providers import OidcProvider

context = CredentialContext(
api_host=config.host,
debug=config.debug,
)

provider = OidcProvider()
result = provider.resolve(context)

if result is not None:
config.api_key["X-Api-Key"] = result.api_key

if config.debug:
click.echo(f"OIDC credential resolved: {result.source_detail}")
elif config.debug:
logger.debug("OIDC auto-discovery did not resolve credentials")
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

OIDC auto-discovery currently doesn’t appear to respect the CLI’s API networking configuration (proxy, SSL verify, custom headers/user-agent). _try_oidc_credential() only passes api_host into OidcProvider, and exchange_oidc_token() uses requests.post(...) with default settings, so users relying on CLOUDSMITH_API_PROXY / CLOUDSMITH_WITHOUT_API_SSL_VERIFY may see OIDC fail even though normal API calls work. Consider threading proxy/verify (or a configured requests.Session) through CredentialContext into the detector/exchange code.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good AI. Let me see what can be done here.

@cloudsmith-iduffy
Copy link
Contributor Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link

Copilot AI commented Feb 28, 2026

@cloudsmith-iduffy I've opened a new pull request, #268, to work on those changes. Once the pull request is ready, I'll request review from you.

cloudsmith-iduffy added a commit that referenced this pull request Feb 28, 2026
…pport

This PR adds credential helper infrastructure for Docker and pip package managers,
enabling automatic authentication to Cloudsmith registries without manual login
commands or embedding credentials in URLs.

This builds upon #267 (OIDC authentication) and extends the credential provider
chain to Docker and pip workflows.

## Features

### Docker Credential Helper
- Command: `cloudsmith credential-helper docker`
- Binary: `docker-credential-cloudsmith` (for Docker CLI integration)
- Supports standard domains: `docker.cloudsmith.io`, `*.docker.cloudsmith.io`
- Supports custom vanity domains via API auto-discovery

### Pip Keyring Backend
- Auto-discovered by pip/twine via `keyring.backends` entry point
- Supports standard domains: `python.cloudsmith.io`, `dl.cloudsmith.io`, `*.cloudsmith.sh`
- Supports custom vanity domains via API auto-discovery
- Priority: 9.9 (runs before system keychains)

### Custom Domain Support
- Set `CLOUDSMITH_ORG=my-org` to enable custom domain discovery
- Fetches domains from `GET /orgs/{org}/custom-domains/` API endpoint
- Caches results in `~/.cloudsmith/cache/custom_domains/` for 1 hour
- Automatic, no manual configuration needed

### Shared Architecture
- Both helpers use the same credential provider chain
- Order: Environment Variable → Config File → Keyring → OIDC
- Consistent authentication behavior across all package managers
- Extensible design for future helpers (npm, cargo, maven, etc.)

## Type of Change
- [x] New feature
- [x] Documentation update

## Backward Compatibility
- ✅ Fully backward compatible
- ✅ No breaking changes
- ✅ Existing authentication methods unchanged
- ✅ Optional features - enable by configuration

## Testing

### Manual Testing - Docker

Environment setup:
```bash
$ env | grep CLOUDSMITH_
CLOUDSMITH_ORG=iduffy-demo
CLOUDSMITH_SERVICE_SLUG=default-v9ty

$ stat ~/.cloudsmith/config.ini
stat: cannot stat '/Users/iduffy/.cloudsmith/config.ini': No such file or directory

$ cloudsmith whoami --verbose
Retrieving your authentication status from the API ... OK

User: default (slug: default-v9ty)

Authentication Method: OIDC Auto-Discovery
  Source: OIDC auto-discovery: AWS (org: iduffy-demo)
  Token Slug: 6FmYSZVQrEho
  Created: 2025-06-07T19:43:47.840466Z

SSO Status: Not configured
  Keyring: Enabled (no tokens stored)
```

Before configuration:
```bash
$ cat ~/.docker/config.json
cat: /Users/iduffy/.docker/config.json: No such file or directory

$ docker pull docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
Error response from daemon: Head "https://docker.cloudsmith.io/v2/iduffy-demo/default/library/ubuntu/manifests/latest": unauthorized
```

After configuration:
```bash
$ cat ~/.docker/config.json
{
  "credHelpers": {
    "docker.cloudsmith.io": "cloudsmith"
  }
}

$ docker pull docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
latest: Pulling from iduffy-demo/default/library/ubuntu
cc43ec4c1381: Pull complete
Digest: sha256:9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8
Status: Downloaded newer image for docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
```

✅ **Success:** Docker authenticated using AWS OIDC credentials via credential helper

### Manual Testing - Pip

Environment setup:
```bash
$ pip config list
:env:.default-timeout='100'
:env:.disable-pip-version-check='1'

$ python -c 'import keyring; print(keyring.backend.get_all_keyring())'
[<keyring.backends.fail.Keyring object at 0x1033a4460>, <cloudsmith_cli.credential_helpers.pip.CloudsmithKeyringBackend object at 0x1033a4c80>, <keyring.backends.chainer.ChainerBackend object at 0x1033a5360>, <keyring.backends.macOS.Keyring object at 0x1033a5720>]
```

Test installation (no credentials in URL):
```bash
$ pip install --no-cache-dir --index-url=https://dl.cloudsmith.io/basic/iduffy-demo/default/python/simple/ cloudsmith-python-native
Looking in indexes: https://dl.cloudsmith.io/basic/iduffy-demo/default/python/simple/
Collecting cloudsmith-python-native
  Downloading https://dl.cloudsmith.io/basic/iduffy-demo/default/python/cloudsmith_python_native-1.0.1050047-py2.py3-none-any.whl (2.2 kB)
Requirement already satisfied: toml in /Users/iduffy/projects/cloudsmith-cli/.venv/lib/python3.10/site-packages (from cloudsmith-python-native) (0.10.2)
Installing collected packages: cloudsmith-python-native
Successfully installed cloudsmith-python-native-1.0.1050047
```

✅ **Success:** Pip authenticated using AWS OIDC credentials via keyring backend

### Automated Testing
- ✅ Pylint: 10.00/10 (perfect score)
- ✅ Tests: 199 passed, 39 skipped
- ✅ No new test failures introduced

## Documentation

Added comprehensive documentation:
- `CUSTOM_DOMAINS.md` - Complete guide for custom domain configuration
- `PIP_KEYRING_POC.md` - Proof of concept guide for pip keyring backend
- `PIP_KEYRING_SUCCESS.md` - Success report with usage examples
- `pip_keyring_poc.sh` - Automated testing script

## Additional Notes

### Architecture Validation
The credential helper architecture was validated by implementing two different
package manager protocols (Docker binary stdin/stdout vs Python keyring API)
to ensure the design is flexible and extensible.

### Custom Domain Support
Custom domains are discovered automatically when `CLOUDSMITH_ORG` is set:
1. First request fetches domains from API
2. Results cached for 1 hour in `~/.cloudsmith/cache/custom_domains/`
3. Subsequent requests use cache (no repeated API calls)
4. Works seamlessly with OIDC in CI/CD environments

### Future Extensions
The architecture is designed to support additional credential helpers:
- npm (`.npmrc` credential helper)
- cargo (Cargo credential helper)
- maven (Maven settings credential helper)
- gradle (Gradle properties credential helper)
- nuget (NuGet credential helper)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cloudsmith-iduffy added a commit that referenced this pull request Feb 28, 2026
…pport

This PR adds credential helper infrastructure for Docker and pip package managers,
enabling automatic authentication to Cloudsmith registries without manual login
commands or embedding credentials in URLs.

This builds upon #267 (OIDC authentication) and extends the credential provider
chain to Docker and pip workflows.

- Command: `cloudsmith credential-helper docker`
- Binary: `docker-credential-cloudsmith` (for Docker CLI integration)
- Supports standard domains: `docker.cloudsmith.io`, `*.docker.cloudsmith.io`
- Supports custom vanity domains via API auto-discovery

- Auto-discovered by pip/twine via `keyring.backends` entry point
- Supports standard domains: `python.cloudsmith.io`, `dl.cloudsmith.io`
- Supports custom vanity domains via API auto-discovery

- Set `CLOUDSMITH_ORG=my-org` to enable custom domain discovery
- Fetches domains from `GET /orgs/{org}/custom-domains/` API endpoint
- Caches results in `~/.cloudsmith/cache/custom_domains/` for 1 hour
- Automatic, no manual configuration needed

- Both helpers use the same credential provider chain
- Order: Environment Variable → Config File → Keyring → OIDC
- Consistent authentication behavior across all package managers
- Extensible design for future helpers (npm, cargo, maven, etc.)

- [x] New feature
- [x] Documentation update

- ✅ Fully backward compatible
- ✅ No breaking changes
- ✅ Existing authentication methods unchanged
- ✅ Optional features - enable by configuration

Environment setup:
```bash
$ env | grep CLOUDSMITH_
CLOUDSMITH_ORG=iduffy-demo
CLOUDSMITH_SERVICE_SLUG=default-v9ty

$ stat ~/.cloudsmith/config.ini
stat: cannot stat '/Users/iduffy/.cloudsmith/config.ini': No such file or directory

$ cloudsmith whoami --verbose
Retrieving your authentication status from the API ... OK

User: default (slug: default-v9ty)

Authentication Method: OIDC Auto-Discovery
  Source: OIDC auto-discovery: AWS (org: iduffy-demo)
  Token Slug: 6FmYSZVQrEho
  Created: 2025-06-07T19:43:47.840466Z

SSO Status: Not configured
  Keyring: Enabled (no tokens stored)
```

Before configuration:
```bash
$ cat ~/.docker/config.json
cat: /Users/iduffy/.docker/config.json: No such file or directory

$ docker pull docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
Error response from daemon: Head "https://docker.cloudsmith.io/v2/iduffy-demo/default/library/ubuntu/manifests/latest": unauthorized
```

After configuration:
```bash
$ cat ~/.docker/config.json
{
  "credHelpers": {
    "docker.cloudsmith.io": "cloudsmith"
  }
}

$ docker pull docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
latest: Pulling from iduffy-demo/default/library/ubuntu
cc43ec4c1381: Pull complete
Digest: sha256:9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8
Status: Downloaded newer image for docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
```

Environment setup:
```bash
$ pip config list
:env:.default-timeout='100'
:env:.disable-pip-version-check='1'

$ python -c 'import keyring; print(keyring.backend.get_all_keyring())'
[<keyring.backends.fail.Keyring object at 0x1033a4460>, <cloudsmith_cli.credential_helpers.pip.CloudsmithKeyringBackend object at 0x1033a4c80>, <keyring.backends.chainer.ChainerBackend object at 0x1033a5360>, <keyring.backends.macOS.Keyring object at 0x1033a5720>]
```

Test installation (no credentials in URL):
```bash
$ pip install --no-cache-dir --index-url=https://dl.cloudsmith.io/basic/iduffy-demo/default/python/simple/ cloudsmith-python-native
Looking in indexes: https://dl.cloudsmith.io/basic/iduffy-demo/default/python/simple/
Collecting cloudsmith-python-native
  Downloading https://dl.cloudsmith.io/basic/iduffy-demo/default/python/cloudsmith_python_native-1.0.1050047-py2.py3-none-any.whl (2.2 kB)
Requirement already satisfied: toml in /Users/iduffy/projects/cloudsmith-cli/.venv/lib/python3.10/site-packages (from cloudsmith-python-native) (0.10.2)
Installing collected packages: cloudsmith-python-native
Successfully installed cloudsmith-python-native-1.0.1050047
```

Custom domains are discovered automatically when `CLOUDSMITH_ORG` is set:
1. First request fetches domains from API
2. Results cached for 1 hour in `~/.cloudsmith/cache/custom_domains/`
3. Subsequent requests use cache (no repeated API calls)
cloudsmith-iduffy added a commit that referenced this pull request Feb 28, 2026
…pport

This PR adds credential helper infrastructure for Docker and pip package managers,
enabling automatic authentication to Cloudsmith registries without manual login
commands or embedding credentials in URLs.

This builds upon #267 (OIDC authentication) and extends the credential provider
chain to Docker and pip workflows.

- Command: `cloudsmith credential-helper docker`
- Binary: `docker-credential-cloudsmith` (for Docker CLI integration)
- Supports standard domains: `docker.cloudsmith.io`, `*.docker.cloudsmith.io`
- Supports custom vanity domains via API auto-discovery

- Auto-discovered by pip/twine via `keyring.backends` entry point
- Supports standard domains: `python.cloudsmith.io`, `dl.cloudsmith.io`
- Supports custom vanity domains via API auto-discovery

- Set `CLOUDSMITH_ORG=my-org` to enable custom domain discovery
- Fetches domains from `GET /orgs/{org}/custom-domains/` API endpoint
- Caches results in `~/.cloudsmith/cache/custom_domains/` for 1 hour
- Automatic, no manual configuration needed

- Both helpers use the same credential provider chain
- Order: Environment Variable → Config File → Keyring → OIDC
- Consistent authentication behavior across all package managers
- Extensible design for future helpers (npm, cargo, maven, etc.)

- [x] New feature
- [x] Documentation update

- ✅ Fully backward compatible
- ✅ No breaking changes
- ✅ Existing authentication methods unchanged
- ✅ Optional features - enable by configuration

Environment setup:
```bash
$ env | grep CLOUDSMITH_
CLOUDSMITH_ORG=iduffy-demo
CLOUDSMITH_SERVICE_SLUG=default-v9ty

$ stat ~/.cloudsmith/config.ini
stat: cannot stat '/Users/iduffy/.cloudsmith/config.ini': No such file or directory

$ cloudsmith whoami --verbose
Retrieving your authentication status from the API ... OK

User: default (slug: default-v9ty)

Authentication Method: OIDC Auto-Discovery
  Source: OIDC auto-discovery: AWS (org: iduffy-demo)
  Token Slug: 6FmYSZVQrEho
  Created: 2025-06-07T19:43:47.840466Z

SSO Status: Not configured
  Keyring: Enabled (no tokens stored)
```

Before configuration:
```bash
$ cat ~/.docker/config.json
cat: /Users/iduffy/.docker/config.json: No such file or directory

$ docker pull docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
Error response from daemon: Head "https://docker.cloudsmith.io/v2/iduffy-demo/default/library/ubuntu/manifests/latest": unauthorized
```

After configuration:
```bash
$ cat ~/.docker/config.json
{
  "credHelpers": {
    "docker.cloudsmith.io": "cloudsmith"
  }
}

$ docker pull docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
latest: Pulling from iduffy-demo/default/library/ubuntu
cc43ec4c1381: Pull complete
Digest: sha256:9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8
Status: Downloaded newer image for docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
```

Environment setup:
```bash
$ pip config list
:env:.default-timeout='100'
:env:.disable-pip-version-check='1'

$ python -c 'import keyring; print(keyring.backend.get_all_keyring())'
[<keyring.backends.fail.Keyring object at 0x1033a4460>, <cloudsmith_cli.credential_helpers.pip.CloudsmithKeyringBackend object at 0x1033a4c80>, <keyring.backends.chainer.ChainerBackend object at 0x1033a5360>, <keyring.backends.macOS.Keyring object at 0x1033a5720>]
```

Test installation (no credentials in URL):
```bash
$ pip install --no-cache-dir --index-url=https://dl.cloudsmith.io/basic/iduffy-demo/default/python/simple/ cloudsmith-python-native
Looking in indexes: https://dl.cloudsmith.io/basic/iduffy-demo/default/python/simple/
Collecting cloudsmith-python-native
  Downloading https://dl.cloudsmith.io/basic/iduffy-demo/default/python/cloudsmith_python_native-1.0.1050047-py2.py3-none-any.whl (2.2 kB)
Requirement already satisfied: toml in /Users/iduffy/projects/cloudsmith-cli/.venv/lib/python3.10/site-packages (from cloudsmith-python-native) (0.10.2)
Installing collected packages: cloudsmith-python-native
Successfully installed cloudsmith-python-native-1.0.1050047
```

Custom domains are discovered automatically when `CLOUDSMITH_ORG` is set:
1. First request fetches domains from API
2. Results cached for 1 hour in `~/.cloudsmith/cache/custom_domains/`
3. Subsequent requests use cache (no repeated API calls)
cloudsmith-iduffy added a commit that referenced this pull request Feb 28, 2026
…pport

This PR adds credential helper infrastructure for Docker and pip package managers,
enabling automatic authentication to Cloudsmith registries without manual login
commands or embedding credentials in URLs.

This builds upon #267 (OIDC authentication) and extends the credential provider
chain to Docker and pip workflows.

- Command: `cloudsmith credential-helper docker`
- Binary: `docker-credential-cloudsmith` (for Docker CLI integration)
- Supports standard domains: `docker.cloudsmith.io`, `*.docker.cloudsmith.io`
- Supports custom vanity domains via API auto-discovery

- Auto-discovered by pip/twine via `keyring.backends` entry point
- Supports standard domains: `python.cloudsmith.io`, `dl.cloudsmith.io`
- Supports custom vanity domains via API auto-discovery

- Set `CLOUDSMITH_ORG=my-org` to enable custom domain discovery
- Fetches domains from `GET /orgs/{org}/custom-domains/` API endpoint
- Caches results in `~/.cloudsmith/cache/custom_domains/` for 1 hour
- Automatic, no manual configuration needed

- Both helpers use the same credential provider chain
- Order: Environment Variable → Config File → Keyring → OIDC
- Consistent authentication behavior across all package managers
- Extensible design for future helpers (npm, cargo, maven, etc.)

- [x] New feature
- [x] Documentation update

- ✅ Fully backward compatible
- ✅ No breaking changes
- ✅ Existing authentication methods unchanged
- ✅ Optional features - enable by configuration

Environment setup:
```bash
$ env | grep CLOUDSMITH_
CLOUDSMITH_ORG=iduffy-demo
CLOUDSMITH_SERVICE_SLUG=default-v9ty

$ stat ~/.cloudsmith/config.ini
stat: cannot stat '/Users/iduffy/.cloudsmith/config.ini': No such file or directory

$ cloudsmith whoami --verbose
Retrieving your authentication status from the API ... OK

User: default (slug: default-v9ty)

Authentication Method: OIDC Auto-Discovery
  Source: OIDC auto-discovery: AWS (org: iduffy-demo)
  Token Slug: 6FmYSZVQrEho
  Created: 2025-06-07T19:43:47.840466Z

SSO Status: Not configured
  Keyring: Enabled (no tokens stored)
```

Before configuration:
```bash
$ cat ~/.docker/config.json
cat: /Users/iduffy/.docker/config.json: No such file or directory

$ docker pull docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
Error response from daemon: Head "https://docker.cloudsmith.io/v2/iduffy-demo/default/library/ubuntu/manifests/latest": unauthorized
```

After configuration:
```bash
$ cat ~/.docker/config.json
{
  "credHelpers": {
    "docker.cloudsmith.io": "cloudsmith"
  }
}

$ docker pull docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
latest: Pulling from iduffy-demo/default/library/ubuntu
cc43ec4c1381: Pull complete
Digest: sha256:9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8
Status: Downloaded newer image for docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
docker.cloudsmith.io/iduffy-demo/default/library/ubuntu:latest
```

Environment setup:
```bash
$ pip config list
:env:.default-timeout='100'
:env:.disable-pip-version-check='1'

$ python -c 'import keyring; print(keyring.backend.get_all_keyring())'
[<keyring.backends.fail.Keyring object at 0x1033a4460>, <cloudsmith_cli.credential_helpers.pip.CloudsmithKeyringBackend object at 0x1033a4c80>, <keyring.backends.chainer.ChainerBackend object at 0x1033a5360>, <keyring.backends.macOS.Keyring object at 0x1033a5720>]
```

Test installation (no credentials in URL):
```bash
$ pip install --no-cache-dir --index-url=https://dl.cloudsmith.io/basic/iduffy-demo/default/python/simple/ cloudsmith-python-native
Looking in indexes: https://dl.cloudsmith.io/basic/iduffy-demo/default/python/simple/
Collecting cloudsmith-python-native
  Downloading https://dl.cloudsmith.io/basic/iduffy-demo/default/python/cloudsmith_python_native-1.0.1050047-py2.py3-none-any.whl (2.2 kB)
Requirement already satisfied: toml in /Users/iduffy/projects/cloudsmith-cli/.venv/lib/python3.10/site-packages (from cloudsmith-python-native) (0.10.2)
Installing collected packages: cloudsmith-python-native
Successfully installed cloudsmith-python-native-1.0.1050047
```

Custom domains are discovered automatically when `CLOUDSMITH_ORG` is set:
1. First request fetches domains from API
2. Results cached for 1 hour in `~/.cloudsmith/cache/custom_domains/`
3. Subsequent requests use cache (no repeated API calls)
Implements AWS-style credential provider chain with automatic OIDC token
discovery for 7 major CI/CD platforms (GitHub Actions, GitLab CI, CircleCI,
Azure DevOps, Bitbucket Pipelines, AWS, and Jenkins/Generic).

Type of Change:
- New feature

Key Features:

- Credential provider chain with priority: CLI flag > env var > config file > keyring > OIDC
- Auto-detects CI/CD environment and exchanges vendor JWT for Cloudsmith token
- Keyring-first token caching with disk fallback
- New `cloudsmith print-token` command to export tokens for curl, docker, etc.
- Platform-specific OIDC detection shown in `whoami --verbose` output

Platform Support:
- GitHub Actions: Fetches token via ACTIONS_ID_TOKEN_REQUEST_URL
- GitLab CI: Reads CI_JOB_JWT_V2 or CI_JOB_JWT
- CircleCI: Reads CIRCLE_OIDC_TOKEN_V2 or CIRCLE_OIDC_TOKEN
- Azure DevOps: Fetches token via SYSTEM_OIDCREQUESTURI
- Bitbucket Pipelines: Reads BITBUCKET_STEP_OIDC_TOKEN
- AWS (ECS/EKS/EC2/Lambda): Uses boto3 credential chain + STS GetWebIdentityToken
- Generic/Jenkins: Reads CLOUDSMITH_OIDC_TOKEN (requires credentials-binding plugin)

Example Usage (AWS):
```bash
$ env | grep CLOUDSMITH_
CLOUDSMITH_ORG=iduffy-demo
CLOUDSMITH_SERVICE_SLUG=default-v9ty

$ stat ~/.cloudsmith/config.ini
stat: cannot stat '/Users/iduffy/.cloudsmith/config.ini': No such file or directory

$ aws sts get-caller-identity
{
    "UserId": "AROA47EXAMPLE:ian@ianduffy.ie",
    "Account": "893EXAMPLE",
    "Arn": "arn:aws:sts::893EXAMPLE:assumed-role/AWSReservedSSO_NOPE_..."
}

$ cloudsmith whoami --verbose
Retrieving your authentication status from the API ... OK

User: default (slug: default-v9ty)

Authentication Method: OIDC Auto-Discovery
  Source: OIDC auto-discovery: AWS (org: iduffy-demo)
  Token Slug: sACcPOv3Iro8
  Created: 2025-06-07T19:43:47.840466Z

💡 Export this token: cloudsmith print-token

$ cloudsmith list repos

$ cloudsmith print-token
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...

$ curl -H "X-Api-Key: $(cloudsmith print-token)" https://api.cloudsmith.io/v1/user/self/

$ docker login docker.cloudsmith.io -u token -p $(cloudsmith print-token)
```

Configuration:
Requires two environment variables:
- CLOUDSMITH_ORG: Organization slug
- CLOUDSMITH_SERVICE_SLUG: Service account slug

Optional:
- CLOUDSMITH_OIDC_AUDIENCE: Override default audience (default: "cloudsmith")
- CLOUDSMITH_NO_KEYRING=1: Skip keyring, use disk cache only

Installation:
- Base install: pip install cloudsmith-cli
- With AWS support: pip install cloudsmith-cli[aws]
- All features: pip install cloudsmith-cli[all]

Security:
- Tokens cached with 60-second expiry margin for auto-refresh
- Cache files ideally use keyring but if they go to disk created with 0o600 permissions
- Exponential backoff with jitter for retry resilience

Backwards Compatibility:

- No breaking changes to existing authentication methods
- OIDC is opt-in via environment variables
- Existing API key/SSO authentication unaffected
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.

3 participants