Skip to content

Conversation

@franklouwers
Copy link

@franklouwers franklouwers commented Nov 27, 2025

CLI Provider Authentication Support for STACKIT SDK

Summary

This PR adds comprehensive support for using STACKIT CLI provider credentials directly in the SDK, enabling seamless authentication across CLI and SDK-based tools without requiring re-authentication. (requires stackitcloud/stackit-cli#1130)

Key Features

  • CLI Credential Integration: New cliauth package enables SDK applications to read and use credentials stored by the STACKIT CLI
  • Automatic Token Refresh: Built-in OAuth2 token refresh with bidirectional sync (tokens refreshed by SDK are written back to storage)
  • Multi-Profile Support: Full support for CLI profiles with automatic resolution from explicit parameters, environment variables, or config files
  • Cross-Platform Storage: Credentials stored in system keyring (macOS Keychain, Linux Secret Service, Windows Credential Manager) with automatic file fallback
  • Thread-Safe: Concurrent request handling with automatic token refresh coordination
  • Background Refresh: Optional background goroutine for proactive token refresh before expiration

Architecture

The implementation consists of four logical commits:

  1. Core Authentication Package (core/cliauth):

    • credentials.go: Credential reading from keyring/file with profile resolution
    • token_refresh.go: OAuth2 token refresh with configurable endpoints
    • flow.go: Thread-safe http.RoundTripper implementation for automatic auth injection
    • background_refresh.go: Background token refresh with context-based lifecycle
    • doc.go: Comprehensive package documentation with usage examples
  2. SDK Configuration Integration (core/config/cli_auth.go):

    • WithCLIProviderAuth(profile): Configure SDK clients to use CLI credentials
    • WithCLIBackgroundTokenRefresh(ctx): Enable background token refresh

Usage Example

// Basic usage with default profile
client, err := dns.NewAPIClient(
    config.WithCLIProviderAuth(""),
)

// With custom profile
client, err := dns.NewAPIClient(
    config.WithCLIProviderAuth("production"),
)

// With background token refresh
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

client, err := dns.NewAPIClient(
    config.WithCLIBackgroundTokenRefresh(ctx),
    config.WithCLIProviderAuth(""),
)

Storage Locations

System Keyring (preferred):

  • Service name: stackit-cli-api (default) or stackit-cli-api/{profile}
  • Platforms: macOS Keychain, Linux Secret Service, Windows Credential Manager

File Fallback:

  • Default profile: ~/.stackit/cli-api-auth-storage.txt
  • Custom profiles: ~/.stackit/profiles/{profile}/cli-api-auth-storage.txt
  • Format: Base64-encoded JSON

Profile Resolution Order

  1. Explicit profile parameter
  2. STACKIT_CLI_PROFILE environment variable
  3. ~/.config/stackit/cli-profile.txt config file
  4. "default" fallback

Backward Compatibility

  • compatible with credentials created by existing STACKIT CLI versions
  • All file paths, formats, and keyring service names match CLI exactly
  • Users can seamlessly switch between CLI and SDK-based tools

Related Issues

Checklist

  • Issue was linked above
  • No generated code was adjusted manually (check comments in file header)
  • Changelogs
    • Changelog in the root directory was adjusted (see here)
    • Changelog(s) of the service(s) were adjusted (see e.g. here)
  • VERSION file(s) of the service(s) were adjusted
  • Code format was applied: make fmt
  • Examples were added / adjusted (see examples/ directory)
  • Unit tests got implemented or updated
  • Unit tests are passing: make test (will be checked by CI)
  • No linter issues: make lint (will be checked by CI)

Add new cliauth package that enables SDK applications to use STACKIT CLI
provider credentials without direct CLI dependency. This implementation
supports:

- Reading credentials from system keyring or file fallback
- Automatic OAuth2 token refresh with configurable endpoints
- Multiple CLI profile support with profile resolution
- Bidirectional credential sync (writeback after refresh)
- Thread-safe RoundTripper implementation for HTTP clients
- Background token refresh with context-based lifecycle management

Storage locations:
- System Keyring (macOS Keychain, Linux Secret Service, Windows Credential Manager)
- File fallback: ~/.stackit/cli-api-auth-storage.txt
Add WithCLIProviderAuth and WithCLIBackgroundTokenRefresh configuration
options to the SDK config package. This enables easy integration of CLI
provider authentication into SDK clients.

Features:
- WithCLIProviderAuth: Configure SDK to use CLI credentials with profile support
- WithCLIBackgroundTokenRefresh: Enable background token refresh with context
- Profile resolution from parameter, env var, config file, or default

Usage example:
  client, err := dns.NewAPIClient(
      config.WithCLIProviderAuth(""),
  )
Add unit tests for CLI provider authentication functionality:

- credentials_test.go: Tests for credential reading, profile resolution,
  keyring/file fallback behavior
- token_refresh_test.go: Tests for token expiration check and refresh logic
- flow_test.go: Tests for RoundTripper implementation and automatic refresh
- cli_auth_test.go: Tests for SDK config integration

Tests cover:
- Profile resolution order (explicit, env var, config file, default)
- Storage location fallback (keyring -> file)
- Token refresh with mock OAuth endpoints
- Thread-safe concurrent access
- Error handling for missing credentials and expired tokens
Fix test expectations to match the actual implementation:
- Update keyring service name from 'stackit-cli-provider' to 'stackit-cli-api'
- Update file paths from 'cli-provider-auth-storage.txt' to 'cli-api-auth-storage.txt'
- Rewrite config tests to match the actual API (profile string instead of provider object)
- Fix helper functions in test files

All tests now pass successfully.
@franklouwers franklouwers requested a review from a team as a code owner November 27, 2025 22:44
@franklouwers
Copy link
Author

Please advise on versioning. Do you have conventions you use?

franklouwers added a commit to franklouwers/terraform-provider-stackit that referenced this pull request Nov 28, 2025
This commit adds support for CLI-based authentication in the Terraform
provider, enabling users to authenticate using credentials from the
STACKIT CLI without managing separate service account credentials.

Changes:
- Add cli_auth boolean attribute to enable CLI authentication
- Add cli_profile string attribute for profile selection
- Implement authentication priority: explicit credentials > CLI > env vars
- Integrate with SDK's WithCLIProviderAuth() configuration option

The implementation follows the explicit opt-in pattern requested in
RFC stackitcloud#880, requiring users to set cli_auth = true to enable the feature.
Profile resolution follows the standard precedence: explicit config >
STACKIT_CLI_PROFILE env var > ~/.config/stackit/cli-profile.txt > default.

This change depends on SDK PR stackitcloud/stackit-sdk-go#3865 which
adds the core CLI authentication functionality, and CLI PR
stackitcloud/stackit-cli#1130 which implements the provider credential
storage.

Closes stackitcloud#719
Related to stackitcloud#880
franklouwers added a commit to franklouwers/terraform-provider-stackit that referenced this pull request Nov 28, 2025
This commit adds support for CLI-based authentication in the Terraform
provider, enabling users to authenticate using credentials from the
STACKIT CLI without managing separate service account credentials.

Changes:
- Add cli_auth boolean attribute to enable CLI authentication
- Add cli_profile string attribute for profile selection
- Implement authentication priority: explicit credentials > CLI > env vars
- Integrate with SDK's WithCLIProviderAuth() configuration option

The implementation follows the explicit opt-in pattern requested in
RFC stackitcloud#880, requiring users to set cli_auth = true to enable the feature.
Profile resolution follows the standard precedence: explicit config >
STACKIT_CLI_PROFILE env var > ~/.config/stackit/cli-profile.txt > default.

This change depends on SDK PR stackitcloud/stackit-sdk-go#3865 which
adds the core CLI authentication functionality, and CLI PR
stackitcloud/stackit-cli#1130 which implements the provider credential
storage.

Closes stackitcloud#719
Related to stackitcloud#880
franklouwers added a commit to franklouwers/terraform-provider-stackit that referenced this pull request Nov 28, 2025
Add replace directive to use SDK fork with CLI authentication support
from PR stackitcloud/stackit-sdk-go#3865 until it's merged and released.

This allows the provider to be built and tested with the CLI auth
functionality before the SDK changes are officially released.

The replace directive references commit 25b6b99bd648 from
github.com/franklouwers/stackit-sdk-go/core which includes the
core/cliauth package and config.WithCLIProviderAuth() function.

Once SDK PR #3865 is merged and a new SDK version is released,
this replace directive should be removed and the provider updated
to require the new SDK version.
Add comprehensive example demonstrating how to use CLI provider
authentication to access STACKIT CLI credentials from applications.

The example covers:
- Default profile authentication
- Specific profile authentication
- Direct credential access for advanced use cases

Also updated the existing authentication example to reference the new
CLI provider authentication option.
@marceljk
Copy link
Contributor

Please advise on versioning. Do you have conventions you use?

@franklouwers Thanks for your contribution across the CLI, SDK and Terraform provider! I created internal tickets to review them soon.

Regarding versioning we do a minor bump, when something is added, like the CLI Authentication. Please adjust the version file to v0.21.0

Create a new entry in core/CHANGELOG.md and in the global CHANGELOG.md, there you can define the changes you made.

@github-actions
Copy link

github-actions bot commented Dec 6, 2025

This PR was marked as stale after 7 days of inactivity and will be closed after another 7 days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it.

@github-actions github-actions bot added the Stale label Dec 6, 2025
@franklouwers
Copy link
Author

Keep open

@github-actions github-actions bot removed the Stale label Dec 7, 2025
@github-actions
Copy link

This PR was marked as stale after 7 days of inactivity and will be closed after another 7 days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it.

Copy link
Contributor

@cgoetz-inovex cgoetz-inovex left a comment

Choose a reason for hiding this comment

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

Thanks for your contribution! Especially splitting into several commits and thorough docs.
config/profile path constructions differs from CLI

Comment on lines +156 to +158
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
containsMiddle(s, substr)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
containsMiddle(s, substr)))
return strings.Index(s, substr) != -1

Comment on lines +158 to +163
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("get home dir: %w", err)
}

profilePath := filepath.Join(homeDir, ".config", "stackit", "cli-profile.txt")
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("get home dir: %w", err)
}
profilePath := filepath.Join(homeDir, ".config", "stackit", "cli-profile.txt")
configDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("get config dir: %w", err)
}
profilePath := filepath.Join(configDir, "stackit", "cli-profile.txt")

current impl fails on MacOS, probably windows

Comment on lines +186 to +194
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("get home dir: %w", err)
}

if profile == defaultProfile {
return filepath.Join(homeDir, ".stackit", "cli-api-auth-storage.txt"), nil
}
return filepath.Join(homeDir, ".stackit", "profiles", profile, "cli-api-auth-storage.txt"), nil
Copy link
Contributor

Choose a reason for hiding this comment

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

//
// If baseTransport is nil, http.DefaultTransport is used.
// If httpClient is nil, a default client is created for token refresh operations.
func NewCLIProviderFlow(profile string, baseTransport http.RoundTripper, httpClient *http.Client) (*CLIProviderFlow, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

to discuss: I'd remove this method in favor of the WithContext variant. Currently used in tests and the examples. I think it would be nice if the examples would show usage without goroutine leaks. It's fine in the example per se because the process only runs for a short amount of time. But nonetheless I'd prefer a leak free example.
When refreshing I also suspect a nil pointer panic, when this variant is used.

Comment on lines +106 to +113
// Check if context was canceled
err := r.flow.refreshContext.Err()
if err != nil {
return fmt.Errorf("context canceled during wait: %w", err)
}

// Sleep briefly before checking again
time.Sleep(r.timeBetweenContextCheck)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Check if context was canceled
err := r.flow.refreshContext.Err()
if err != nil {
return fmt.Errorf("context canceled during wait: %w", err)
}
// Sleep briefly before checking again
time.Sleep(r.timeBetweenContextCheck)
select {
case <- r.flow.refreshContext.Done():
return r.flow.refreshContext.Err()
case <- time.After(r.timeBetweenContextCheck):
// do nothing
}

Not sure if this one matters, because the default timeBetweenContextCheck is just a second.

}

// Wait for background refresh to trigger
time.Sleep(3 * time.Second)
Copy link
Contributor

Choose a reason for hiding this comment

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

slow test, we could use https://pkg.go.dev/testing/synctest here to circumvent this sleep. But it was only added in go 1.24. We'd have to add this version to CI and guard the test with a build tag to only execute on 1.24.
Not sure if worth it.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support CLI Authentication Fallback in the STACKIT Terraform Provider

3 participants