@@ -18,6 +18,7 @@ import (
18
18
"github.com/stacklok/toolhive/pkg/auth"
19
19
"github.com/stacklok/toolhive/pkg/auth/discovery"
20
20
"github.com/stacklok/toolhive/pkg/auth/oauth"
21
+ "github.com/stacklok/toolhive/pkg/auth/tokenexchange"
21
22
"github.com/stacklok/toolhive/pkg/logger"
22
23
"github.com/stacklok/toolhive/pkg/networking"
23
24
"github.com/stacklok/toolhive/pkg/transport"
@@ -121,6 +122,8 @@ var (
121
122
const (
122
123
// #nosec G101 - this is an environment variable name, not a credential
123
124
envOAuthClientSecret = "TOOLHIVE_REMOTE_OAUTH_CLIENT_SECRET"
125
+ // #nosec G101 - this is an environment variable name, not a credential
126
+ envTokenExchangeClientSecret = "TOOLHIVE_TOKEN_EXCHANGE_CLIENT_SECRET"
124
127
)
125
128
126
129
func init () {
@@ -227,8 +230,19 @@ func proxyCmdFunc(cmd *cobra.Command, args []string) error {
227
230
}
228
231
middlewares = append (middlewares , authMiddleware )
229
232
230
- // Add OAuth token injection middleware for outgoing requests if we have an access token
231
- if tokenSource != nil {
233
+ // Add OAuth token injection or token exchange middleware for outgoing requests
234
+ if remoteAuthFlags .TokenExchangeURL != "" {
235
+ // Use token exchange middleware when token exchange is configured
236
+ tokenExchangeConfig := createTokenExchangeConfig ()
237
+ if tokenExchangeConfig != nil {
238
+ tokenExchangeMiddleware , teMwErr := tokenexchange .CreateTokenExchangeMiddlewareFromClaims (* tokenExchangeConfig )
239
+ if teMwErr != nil {
240
+ return fmt .Errorf ("failed to create token exchange middleware: %v" , teMwErr )
241
+ }
242
+ middlewares = append (middlewares , tokenExchangeMiddleware )
243
+ }
244
+ } else if tokenSource != nil {
245
+ // Fallback to direct token injection when no token exchange is configured
232
246
tokenMiddleware := createTokenInjectionMiddleware (tokenSource )
233
247
middlewares = append (middlewares , tokenMiddleware )
234
248
}
@@ -346,43 +360,70 @@ func handleOutgoingAuthentication(ctx context.Context) (*oauth2.TokenSource, *oa
346
360
return nil , nil , nil // No authentication required
347
361
}
348
362
349
- // resolveClientSecret resolves the OAuth client secret from multiple sources
350
- // Priority: 1. Flag value, 2. File, 3. Environment variable
351
- func resolveClientSecret () (string , error ) {
363
+ // resolveSecretFromSources resolves a secret from multiple sources with priority ordering
364
+ // Priority: 1. Direct value (flag), 2. File path, 3. Environment variable
365
+ // Returns empty string if no source provides a value (not an error)
366
+ func resolveSecretFromSources (directValue , filePath , envVarName , secretType string ) (string , error ) {
352
367
// 1. Check if provided directly via flag
353
- if remoteAuthFlags . RemoteAuthClientSecret != "" {
354
- logger .Debug ("Using client secret from command-line flag" )
355
- return remoteAuthFlags . RemoteAuthClientSecret , nil
368
+ if directValue != "" {
369
+ logger .Debugf ("Using %s from command-line flag" , secretType )
370
+ return directValue , nil
356
371
}
357
372
358
373
// 2. Check if provided via file
359
- if remoteAuthFlags . RemoteAuthClientSecretFile != "" {
374
+ if filePath != "" {
360
375
// Clean the file path to prevent path traversal
361
- cleanPath := filepath .Clean (remoteAuthFlags . RemoteAuthClientSecretFile )
362
- logger .Debugf ("Reading client secret from file: %s" , cleanPath )
376
+ cleanPath := filepath .Clean (filePath )
377
+ logger .Debugf ("Reading %s from file: %s" , secretType , cleanPath )
363
378
// #nosec G304 - file path is cleaned above
364
379
secretBytes , err := os .ReadFile (cleanPath )
365
380
if err != nil {
366
- return "" , fmt .Errorf ("failed to read client secret file %s: %w" , cleanPath , err )
381
+ return "" , fmt .Errorf ("failed to read %s file %s: %w" , secretType , cleanPath , err )
367
382
}
368
383
secret := strings .TrimSpace (string (secretBytes ))
369
384
if secret == "" {
370
- return "" , fmt .Errorf ("client secret file %s is empty" , cleanPath )
385
+ return "" , fmt .Errorf ("%s file %s is empty" , secretType , cleanPath )
371
386
}
372
387
return secret , nil
373
388
}
374
389
375
390
// 3. Check environment variable
376
- if secret := os .Getenv (envOAuthClientSecret ); secret != "" {
377
- logger .Debugf ("Using client secret from %s environment variable" , envOAuthClientSecret )
378
- return secret , nil
391
+ if envVarName != "" {
392
+ if secret := os .Getenv (envVarName ); secret != "" {
393
+ logger .Debugf ("Using %s from %s environment variable" , secretType , envVarName )
394
+ return secret , nil
395
+ }
379
396
}
380
397
381
- // No client secret found - this is acceptable for PKCE flows
382
- logger .Debug ("No client secret provided - using PKCE flow" )
398
+ // No secret found - return empty string (caller decides if this is an error)
399
+ logger .Debugf ("No %s provided" , secretType )
383
400
return "" , nil
384
401
}
385
402
403
+ // resolveClientSecret resolves the OAuth client secret from multiple sources
404
+ // Priority: 1. Flag value, 2. File, 3. Environment variable
405
+ func resolveClientSecret () (string , error ) {
406
+ secret , err := resolveSecretFromSources (
407
+ remoteAuthFlags .RemoteAuthClientSecret ,
408
+ remoteAuthFlags .RemoteAuthClientSecretFile ,
409
+ envOAuthClientSecret ,
410
+ "client secret" ,
411
+ )
412
+ if err != nil {
413
+ return "" , err
414
+ }
415
+ if secret == "" {
416
+ // No client secret found - this is acceptable for PKCE flows
417
+ logger .Debug ("No client secret provided - using PKCE flow" )
418
+ }
419
+ return secret , nil
420
+ }
421
+
422
+ // createTokenExchangeConfig creates a TokenExchangeConfig from remoteAuthFlags
423
+ func createTokenExchangeConfig () * tokenexchange.Config {
424
+ return remoteAuthFlags .BuildTokenExchangeConfig ()
425
+ }
426
+
386
427
// createTokenInjectionMiddleware creates a middleware that injects the OAuth token into requests
387
428
func createTokenInjectionMiddleware (tokenSource * oauth2.TokenSource ) types.MiddlewareFunction {
388
429
return func (next http.Handler ) http.Handler {
0 commit comments