@@ -14,23 +14,54 @@ import (
1414 "github.com/stacklok/toolhive/pkg/runner"
1515)
1616
17+ const (
18+ // #nosec G101 - this is an environment variable name, not a credential
19+ envTokenExchangeClientSecret = "TOOLHIVE_TOKEN_EXCHANGE_CLIENT_SECRET"
20+ )
21+
1722// readSecretFromFile reads a secret from a file, cleaning the path and trimming whitespace
18- func readSecretFromFile (filePath , secretType string ) (string , error ) {
23+ func readSecretFromFile (filePath string ) (string , error ) {
1924 // Clean the file path to prevent path traversal
2025 cleanPath := filepath .Clean (filePath )
21- logger .Debugf ("Reading %s from file: %s" , secretType , cleanPath )
26+ logger .Debugf ("Reading secret from file: %s" , cleanPath )
2227 // #nosec G304 - file path is cleaned above
2328 secretBytes , err := os .ReadFile (cleanPath )
2429 if err != nil {
25- return "" , fmt .Errorf ("failed to read %s file %s: %w" , secretType , cleanPath , err )
30+ return "" , fmt .Errorf ("failed to read secret file %s: %w" , cleanPath , err )
2631 }
2732 secret := strings .TrimSpace (string (secretBytes ))
2833 if secret == "" {
29- return "" , fmt .Errorf ("%s file %s is empty" , secretType , cleanPath )
34+ return "" , fmt .Errorf ("secret file %s is empty" , cleanPath )
3035 }
3136 return secret , nil
3237}
3338
39+ // resolveSecret resolves a secret from multiple sources following a standard priority order.
40+ // Priority: 1. Flag value, 2. File, 3. Environment variable
41+ // Returns empty string (not an error) if no secret is found - this is acceptable for public client/PKCE flows.
42+ func resolveSecret (flagValue , filePath , envVarName string ) (string , error ) {
43+ // 1. Check if provided directly via flag
44+ if flagValue != "" {
45+ logger .Debug ("Using secret from command-line flag" )
46+ return flagValue , nil
47+ }
48+
49+ // 2. Check if provided via file
50+ if filePath != "" {
51+ return readSecretFromFile (filePath )
52+ }
53+
54+ // 3. Check environment variable
55+ if secret := os .Getenv (envVarName ); secret != "" {
56+ logger .Debugf ("Using secret from %s environment variable" , envVarName )
57+ return secret , nil
58+ }
59+
60+ // No secret found - this is acceptable for PKCE flows
61+ logger .Debug ("No secret provided - using public client mode" )
62+ return "" , nil
63+ }
64+
3465// RemoteAuthFlags holds the common remote authentication configuration
3566type RemoteAuthFlags struct {
3667 EnableRemoteAuth bool
@@ -65,21 +96,14 @@ func (f *RemoteAuthFlags) BuildTokenExchangeConfig() (*tokenexchange.Config, err
6596 return nil , nil
6697 }
6798
68- // Resolve token exchange client secret from flag or file only
69- // Environment variable is handled by the middleware for Kubernetes deployments
70- var clientSecret string
71- if f .TokenExchangeClientSecret != "" {
72- clientSecret = f .TokenExchangeClientSecret
73- logger .Debug ("Using token exchange client secret from command-line flag" )
74- } else if f .TokenExchangeClientSecretFile != "" {
75- var err error
76- clientSecret , err = readSecretFromFile (f .TokenExchangeClientSecretFile , "token exchange client secret" )
77- if err != nil {
78- return nil , err
79- }
80- } else {
81- // Empty client secret is acceptable for public client mode
82- logger .Debug ("No token exchange client secret provided - using public client mode" )
99+ // Resolve token exchange client secret using the same mechanism as remote-auth-client-secret
100+ clientSecret , err := resolveSecret (
101+ f .TokenExchangeClientSecret ,
102+ f .TokenExchangeClientSecretFile ,
103+ envTokenExchangeClientSecret ,
104+ )
105+ if err != nil {
106+ return nil , err
83107 }
84108
85109 // Determine header strategy based on whether custom header name is provided
0 commit comments