Skip to content

Conversation

@scott-cotton
Copy link
Member

@scott-cotton scott-cotton commented Dec 5, 2025

Summary

Stacked on #278

Refactors authentication logic into a unified shared package, fixes a bug where devbox sessions were being double-claimed when a devbox ID was explicitly provided, fixes daemon viper initialization causing incorrect auth resolution, and adds comprehensive debug logging for auth diagnostics.

Key Changes

Auth Refactoring

  • Unified API Client Creation - Extracted createAPIClient function from internal/devbox/session_manager.go into a new shared package internal/locald/sandboxmanager/apiclient/apiclient.go
  • Centralized Auth Logic - All API client creation now uses apiclient.CreateAPIClient() which handles:
    • Dynamic auth resolution (API keys and bearer tokens)
    • Bearer token refresh when expired
    • Proper API URL configuration
    • Fallback to CI config API key
  • Unified Auth Headers - Added apiclient.GetAuthHeaders() function for consistent auth header resolution across components
  • Code Reuse - Both SessionManager and SandboxManager now use the same unified auth mechanism, eliminating code duplication

Bug Fix: Double Devbox Claim Prevention

  • Issue: When a devbox ID was provided via --devbox flag, the code would attempt to claim the devbox session even though devbox.GetID() already handles claiming when no devbox ID is provided
  • Fix: Added a claimed boolean flag to track whether the devbox was already claimed during ID retrieval, preventing duplicate claim attempts
  • Impact: Prevents unnecessary API calls and potential errors when connecting with an explicit devbox ID

Bug Fix: Daemon Viper Initialization

  • Issue: The daemon process (locald) was not initializing viper with the config file specified via --config flag. This caused auth.ResolveAuth() to read from the wrong config file (default ~/.signadot/config.yaml instead of the specified config), leading to incorrect auth resolution (e.g., trying to use API keys from wrong config when bearer token auth should be used)
  • Fix:
    • Added ConfigFile field to ConnectInvocationConfig to pass the config file path from CLI to daemon
    • Added initViperFromCIConfig() helper function that initializes viper with the correct config file before auth resolution
    • Call viper initialization early in RunSandboxManager() before creating components that use auth
  • Impact: Ensures daemon uses the same config file as the CLI, preventing 401 Unauthorized errors when using bearer token auth with non-default config files

Debug Logging Enhancements

  • Comprehensive Auth Logging: Added detailed debug logging throughout the auth flow:
    • Auth resolution source (config/keyring/plaintext)
    • Auth type (API key vs bearer token)
    • Bearer token expiration status
    • API URL resolution (source: ciConfig/viper/default)
    • Token refresh attempts and results
    • API call details (endpoint, parameters, response codes)
  • Logger Handling: Refactored to use slog.Default() when no logger provided, eliminating nil checks throughout the codebase
  • Impact: Enables comprehensive diagnostics for auth-related issues without requiring code changes

Technical Details

  • Files Changed: 7 files modified, 1 new file created
  • Code Movement: ~170 lines of auth logic moved from session_manager.go to shared apiclient package
  • Viper Initialization: Daemon now properly initializes viper with config file path from ciConfig before any auth resolution
  • Logging: All logging uses structured logging with slog, with automatic fallback to slog.Default() when no logger provided
  • Backward Compatible: No breaking changes - existing functionality preserved
  • Improved Maintainability: Single source of truth for API client creation and auth handling

Testing Notes

  • Verified that devbox connection works with explicit --devbox flag without double-claiming
  • Verified that devbox connection works without --devbox flag (auto-registration/retrieval)
  • Verified that bearer token refresh still works correctly
  • Verified that API key fallback still works correctly
  • Verified that daemon correctly uses config file specified via --config flag
  • Verified that auth resolution works correctly in daemon context with bearer tokens from keyring
  • Verified debug logging provides comprehensive diagnostic information for auth issues

Comment on lines 34 to 35
// initViperFromCIConfig initializes viper with the config file path from ciConfig
func initViperFromCIConfig(ciConfig *config.ConnectInvocationConfig) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a lot of duplication in this func vs:

if c.ConfigFile != "" {
viper.SetConfigFile(c.ConfigFile)
} else {
signadotDir, err := system.GetSignadotDir()
if err != nil {
return err
}
viper.AddConfigPath(signadotDir)
viper.SetConfigName("config") // Doesn't include extension.
viper.SetConfigType("yaml") // File name will be "config.yaml".
}
viper.SetEnvPrefix("signadot")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
// The config file is optional (required params (org, apikey) can
// be set by env var instead).
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return fmt.Errorf("error reading config file: %w", err)
}
}

Why don't we centralize this?

Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't build:

$ go build ./cmd/signadot
# github.com/signadot/cli/internal/locald/sandboxmanager/apiclient
internal/locald/sandboxmanager/apiclient/apiclient.go:109:6: authType redeclared in this block
	internal/locald/sandboxmanager/apiclient/apiclient.go:47:2: other declaration of authType

Copy link
Member Author

Choose a reason for hiding this comment

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

sorry I thought the agent checked for that after nudging it to clean up...

}

// CreateAPIClientWithLogger creates a unified API client with logging support
func CreateAPIClientWithLogger(ciConfig *config.ConnectInvocationConfig, authInfo *auth.ResolvedAuth, log *slog.Logger) (*client.SignadotAPI, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO, this func deserves some normalization (repeated if statements, repeted logic, etc), e.g.:

// Get API URL - prefer ciConfig (passed from connect command), then viper, then default
// Note: In daemon context, viper may not be initialized, so ciConfig.APIURL is preferred
apiURL := "https://api.signadot.com"
apiURLSource := "default"
if ciConfig != nil && ciConfig.APIURL != "" {
apiURL = ciConfig.APIURL
apiURLSource = "ciConfig"
} else if apiURLFromViper := viper.GetString("api_url"); apiURLFromViper != "" {
apiURL = apiURLFromViper
apiURLSource = "viper"
}
log.Debug("CreateAPIClient: API URL resolved",
"apiURL", apiURL,
"source", apiURLSource,
"ciConfigHasAPIURL", ciConfig != nil && ciConfig.APIURL != "",
"viperHasAPIURL", viper.GetString("api_url") != "")
// Create transport config
cfg := &transport.APIConfig{
APIURL: apiURL,
UserAgent: fmt.Sprintf("signadot-cli:%s", buildinfo.Version),
Debug: false,
}

vs

// Create an unauthenticated API client for the refresh call
apiURL := "https://api.signadot.com"
apiURLSource := "default"
if apiURLFromViper := viper.GetString("api_url"); apiURLFromViper != "" {
apiURL = apiURLFromViper
apiURLSource = "viper"
}
log.Debug("refreshBearerToken: using API URL for refresh",
"apiURL", apiURL,
"source", apiURLSource,
"hasRefreshToken", authInfo.RefreshToken != "")
cfg := &transport.APIConfig{
APIURL: apiURL,
UserAgent: fmt.Sprintf("signadot-cli:%s", buildinfo.Version),
Debug: false,
}

These are pretty much the same, and in the first case we use ciConfig while in the second we don't.

}

// refreshBearerToken refreshes an expired bearer token using the refresh token
func refreshBearerToken(authInfo *auth.ResolvedAuth, log *slog.Logger) (*auth.ResolvedAuth, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@scott-cotton, did you test the refresh token works fine?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes

@scott-cotton
Copy link
Member Author

@daniel-de-vera comments addressed

Copy link
Contributor

@daniel-de-vera daniel-de-vera left a comment

Choose a reason for hiding this comment

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

LGTM

@scott-cotton scott-cotton merged commit b5a37e5 into rm-register-sbx-rpc Dec 8, 2025
@scott-cotton scott-cotton deleted the auth-flow-fixes branch December 8, 2025 14:18
scott-cotton added a commit that referenced this pull request Dec 8, 2025
* remove the rpc for register sandbox, it is now unused

* Auth and flow fixes (#279)

* refactor auth and fix a default flow issue that crept in

* add viper init with config file info in sbmgr and debug logging for auth and devbox sessions

* CR followup
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.

3 participants