Skip to content

Commit ff5d970

Browse files
authored
Support non-OIDC OAuth in thv proxy (#1312)
1 parent 91eb4f9 commit ff5d970

File tree

5 files changed

+517
-20
lines changed

5 files changed

+517
-20
lines changed

cmd/thv/app/proxy.go

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,22 @@ Basic transparent proxy:
6060
6161
thv proxy my-server --target-uri http://localhost:8080
6262
63-
Proxy with OAuth authentication to remote server:
63+
Proxy with OIDC authentication to remote server:
6464
6565
thv proxy my-server --target-uri https://api.example.com \
6666
--remote-auth --remote-auth-issuer https://auth.example.com \
6767
--remote-auth-client-id my-client-id \
6868
--remote-auth-client-secret-file /path/to/secret
6969
70+
Proxy with non-OIDC OAuth authentication to remote server:
71+
72+
thv proxy my-server --target-uri https://api.example.com \
73+
--remote-auth \
74+
--remote-auth-authorize-url https://auth.example.com/oauth/authorize \
75+
--remote-auth-token-url https://auth.example.com/oauth/token \
76+
--remote-auth-client-id my-client-id \
77+
--remote-auth-client-secret-file /path/to/secret
78+
7079
Proxy with OIDC protection for incoming requests:
7180
7281
thv proxy my-server --target-uri http://localhost:8080 \
@@ -98,6 +107,10 @@ var (
98107
remoteAuthTimeout time.Duration
99108
remoteAuthCallbackPort int
100109
enableRemoteAuth bool
110+
111+
// Manual OAuth endpoint configuration
112+
remoteAuthAuthorizeURL string
113+
remoteAuthTokenURL string
101114
)
102115

103116
// Default timeout constants
@@ -141,14 +154,18 @@ func init() {
141154
"OAuth client secret for remote server authentication (optional for PKCE)")
142155
proxyCmd.Flags().StringVar(&remoteAuthClientSecretFile, "remote-auth-client-secret-file", "",
143156
"Path to file containing OAuth client secret (alternative to --remote-auth-client-secret)")
144-
proxyCmd.Flags().StringSliceVar(&remoteAuthScopes, "remote-auth-scopes",
145-
[]string{"openid", "profile", "email"}, "OAuth scopes to request for remote server authentication")
157+
proxyCmd.Flags().StringSliceVar(&remoteAuthScopes, "remote-auth-scopes", []string{},
158+
"OAuth scopes to request for remote server authentication (defaults: OIDC uses 'openid,profile,email')")
146159
proxyCmd.Flags().BoolVar(&remoteAuthSkipBrowser, "remote-auth-skip-browser", false,
147160
"Skip opening browser for remote server OAuth flow")
148161
proxyCmd.Flags().DurationVar(&remoteAuthTimeout, "remote-auth-timeout", 30*time.Second,
149162
"Timeout for OAuth authentication flow (e.g., 30s, 1m, 2m30s)")
150163
proxyCmd.Flags().IntVar(&remoteAuthCallbackPort, "remote-auth-callback-port", 8666,
151164
"Port for OAuth callback server during remote authentication (default: 8666)")
165+
proxyCmd.Flags().StringVar(&remoteAuthAuthorizeURL, "remote-auth-authorize-url", "",
166+
"OAuth authorization endpoint URL (alternative to --remote-auth-issuer for non-OIDC OAuth)")
167+
proxyCmd.Flags().StringVar(&remoteAuthTokenURL, "remote-auth-token-url", "",
168+
"OAuth token endpoint URL (alternative to --remote-auth-issuer for non-OIDC OAuth)")
152169

153170
// Mark target-uri as required
154171
if err := proxyCmd.MarkFlagRequired("target-uri"); err != nil {
@@ -383,16 +400,34 @@ func performOAuthFlow(ctx context.Context, issuer, clientID, clientSecret string
383400
scopes []string) (*oauth2.TokenSource, *oauth.Config, error) {
384401
logger.Info("Starting OAuth authentication flow...")
385402

386-
// Create OAuth config from OIDC discovery
387-
oauthConfig, err := oauth.CreateOAuthConfigFromOIDC(
388-
ctx,
389-
issuer,
390-
clientID,
391-
clientSecret,
392-
scopes,
393-
true, // Enable PKCE by default for security
394-
remoteAuthCallbackPort,
395-
)
403+
var oauthConfig *oauth.Config
404+
var err error
405+
406+
// Check if we have manual OAuth endpoints configured
407+
if remoteAuthAuthorizeURL != "" && remoteAuthTokenURL != "" {
408+
logger.Info("Using manual OAuth configuration")
409+
oauthConfig, err = oauth.CreateOAuthConfigManual(
410+
clientID,
411+
clientSecret,
412+
remoteAuthAuthorizeURL,
413+
remoteAuthTokenURL,
414+
scopes,
415+
true, // Enable PKCE by default for security
416+
remoteAuthCallbackPort,
417+
)
418+
} else {
419+
// Fall back to OIDC discovery
420+
logger.Info("Using OIDC discovery")
421+
oauthConfig, err = oauth.CreateOAuthConfigFromOIDC(
422+
ctx,
423+
issuer,
424+
clientID,
425+
clientSecret,
426+
scopes,
427+
true, // Enable PKCE by default for security
428+
remoteAuthCallbackPort,
429+
)
430+
}
396431
if err != nil {
397432
return nil, nil, fmt.Errorf("failed to create OAuth config: %w", err)
398433
}
@@ -454,14 +489,24 @@ func handleOutgoingAuthentication(ctx context.Context) (*oauth2.TokenSource, *oa
454489
}
455490

456491
if enableRemoteAuth {
457-
// If OAuth is explicitly enabled, use provided configuration
458-
if remoteAuthIssuer == "" {
459-
return nil, nil, fmt.Errorf("remote-auth-issuer is required when remote authentication is enabled")
460-
}
492+
// If OAuth is explicitly enabled, validate configuration
461493
if remoteAuthClientID == "" {
462494
return nil, nil, fmt.Errorf("remote-auth-client-id is required when remote authentication is enabled")
463495
}
464496

497+
// Check if we have either OIDC issuer or manual OAuth endpoints
498+
hasOIDCConfig := remoteAuthIssuer != ""
499+
hasManualConfig := remoteAuthAuthorizeURL != "" && remoteAuthTokenURL != ""
500+
501+
if !hasOIDCConfig && !hasManualConfig {
502+
return nil, nil, fmt.Errorf("either --remote-auth-issuer (for OIDC) or both --remote-auth-authorize-url " +
503+
"and --remote-auth-token-url (for OAuth) are required")
504+
}
505+
506+
if hasOIDCConfig && hasManualConfig {
507+
return nil, nil, fmt.Errorf("cannot specify both OIDC issuer and manual OAuth endpoints - choose one approach")
508+
}
509+
465510
return performOAuthFlow(ctx, remoteAuthIssuer, remoteAuthClientID, clientSecret, remoteAuthScopes)
466511
}
467512

docs/cli/thv_proxy.md

Lines changed: 13 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/auth/oauth/manual.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Package oauth provides OAuth 2.0 and OIDC authentication functionality.
2+
package oauth
3+
4+
import (
5+
"fmt"
6+
7+
"github.com/stacklok/toolhive/pkg/networking"
8+
)
9+
10+
// CreateOAuthConfigManual creates an OAuth config with manually provided endpoints
11+
func CreateOAuthConfigManual(
12+
clientID, clientSecret string,
13+
authURL, tokenURL string,
14+
scopes []string,
15+
usePKCE bool,
16+
callbackPort int,
17+
) (*Config, error) {
18+
if clientID == "" {
19+
return nil, fmt.Errorf("client ID is required")
20+
}
21+
if authURL == "" {
22+
return nil, fmt.Errorf("authorization URL is required")
23+
}
24+
if tokenURL == "" {
25+
return nil, fmt.Errorf("token URL is required")
26+
}
27+
28+
// Validate URLs
29+
if err := networking.ValidateEndpointURL(authURL); err != nil {
30+
return nil, fmt.Errorf("invalid authorization URL: %w", err)
31+
}
32+
if err := networking.ValidateEndpointURL(tokenURL); err != nil {
33+
return nil, fmt.Errorf("invalid token URL: %w", err)
34+
}
35+
36+
// Default scopes for regular OAuth (don't assume OIDC scopes)
37+
if len(scopes) == 0 {
38+
scopes = []string{}
39+
}
40+
41+
return &Config{
42+
ClientID: clientID,
43+
ClientSecret: clientSecret,
44+
AuthURL: authURL,
45+
TokenURL: tokenURL,
46+
Scopes: scopes,
47+
UsePKCE: usePKCE,
48+
CallbackPort: callbackPort,
49+
}, nil
50+
}

0 commit comments

Comments
 (0)