diff --git a/example/proxy_basic_auth.go b/example/proxy_basic_auth.go index 680c7e1..fab13c1 100644 --- a/example/proxy_basic_auth.go +++ b/example/proxy_basic_auth.go @@ -52,8 +52,8 @@ func main() { }, } - // Create access token - token := pivnet.NewAccessTokenOrLegacyToken(apiToken, config.Host, config.SkipSSLValidation) + // Create access token with proxy auth config + token := pivnet.NewAccessTokenOrLegacyTokenWithProxy(apiToken, config.Host, config.SkipSSLValidation, config.ProxyAuthConfig) // Create the client with proxy support client, err := pivnet.NewClientWithProxy(token, config, logger) diff --git a/example/proxy_spnego_auth.go b/example/proxy_spnego_auth.go index adac20a..ace320d 100644 --- a/example/proxy_spnego_auth.go +++ b/example/proxy_spnego_auth.go @@ -56,8 +56,8 @@ func main() { }, } - // Create access token - token := pivnet.NewAccessTokenOrLegacyToken(apiToken, config.Host, config.SkipSSLValidation) + // Create access token with proxy auth config + token := pivnet.NewAccessTokenOrLegacyTokenWithProxy(apiToken, config.Host, config.SkipSSLValidation, config.ProxyAuthConfig) // Create the client with proxy support fmt.Println("Initializing client with SPNEGO proxy authentication...") diff --git a/pivnet.go b/pivnet.go index e387153..1126b3c 100644 --- a/pivnet.go +++ b/pivnet.go @@ -57,6 +57,7 @@ type AccessTokenOrLegacyToken struct { refreshToken string skipSSLValidation bool userAgent string + proxyAuthConfig ProxyAuthConfig } type QueryParameter struct { @@ -68,7 +69,7 @@ func (o AccessTokenOrLegacyToken) AccessToken() (string, error) { const legacyAPITokenLength = 20 if len(o.refreshToken) > legacyAPITokenLength { baseURL := fmt.Sprintf("%s%s", o.host, apiVersion) - tokenFetcher := NewTokenFetcher(baseURL, o.refreshToken, o.skipSSLValidation, o.userAgent) + tokenFetcher := NewTokenFetcher(baseURL, o.refreshToken, o.skipSSLValidation, o.userAgent, o.proxyAuthConfig) accessToken, err := tokenFetcher.GetToken() if err != nil { @@ -121,6 +122,22 @@ func NewAccessTokenOrLegacyToken(token string, host string, skipSSLValidation bo host: host, skipSSLValidation: skipSSLValidation, userAgent: userAgent, + proxyAuthConfig: ProxyAuthConfig{}, + } +} + +// NewAccessTokenOrLegacyTokenWithProxy creates an AccessTokenOrLegacyToken with proxy authentication support +func NewAccessTokenOrLegacyTokenWithProxy(token string, host string, skipSSLValidation bool, proxyAuthConfig ProxyAuthConfig, userAgentOptional ...string) AccessTokenOrLegacyToken { + var userAgent = "" + if len(userAgentOptional) > 0 { + userAgent = userAgentOptional[0] + } + return AccessTokenOrLegacyToken{ + refreshToken: token, + host: host, + skipSSLValidation: skipSSLValidation, + userAgent: userAgent, + proxyAuthConfig: proxyAuthConfig, } } diff --git a/uaa.go b/uaa.go index 05bef8c..aba5313 100644 --- a/uaa.go +++ b/uaa.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "time" ) @@ -18,22 +19,62 @@ type TokenFetcher struct { RefreshToken string SkipSSLValidation bool UserAgent string + ProxyAuthConfig ProxyAuthConfig } -func NewTokenFetcher(endpoint, refreshToken string, skipSSLValidation bool, userAgent string) *TokenFetcher { - return &TokenFetcher{endpoint, refreshToken, skipSSLValidation, userAgent } +func NewTokenFetcher(endpoint, refreshToken string, skipSSLValidation bool, userAgent string, proxyAuthConfig ProxyAuthConfig) *TokenFetcher { + return &TokenFetcher{endpoint, refreshToken, skipSSLValidation, userAgent, proxyAuthConfig} } func (t TokenFetcher) GetToken() (string, error) { - httpClient := &http.Client{ - Timeout: 60 * time.Second, - Transport: &http.Transport{ + var transport http.RoundTripper + var err error + + // If proxy authentication is configured, use it; otherwise use standard transport + if t.ProxyAuthConfig.AuthType != "" { + // Create base transport + baseTransport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: t.SkipSSLValidation, + }, + } + + // Parse proxy URL + if t.ProxyAuthConfig.ProxyURL == "" { + return "", fmt.Errorf("proxy URL is required when proxy authentication is specified") + } + proxyURL, err := url.Parse(t.ProxyAuthConfig.ProxyURL) + if err != nil { + return "", fmt.Errorf("failed to parse proxy URL: %w", err) + } + baseTransport.Proxy = http.ProxyURL(proxyURL) + + // Create authenticator + authenticator, err := NewProxyAuthenticator(t.ProxyAuthConfig) + if err != nil { + return "", fmt.Errorf("failed to create proxy authenticator: %w", err) + } + + // Wrap transport with proxy authentication + transport, err = NewProxyAuthTransport(baseTransport, authenticator) + if err != nil { + return "", fmt.Errorf("failed to initialize proxy authentication: %w", err) + } + } else { + // Use standard transport with environment proxy support + transport = &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: t.SkipSSLValidation, }, Proxy: http.ProxyFromEnvironment, - }, + } + } + + httpClient := &http.Client{ + Timeout: 60 * time.Second, + Transport: transport, } + body := AuthBody{RefreshToken: t.RefreshToken} b, err := json.Marshal(body) if err != nil { diff --git a/uaa_test.go b/uaa_test.go index 6cacd84..2934182 100644 --- a/uaa_test.go +++ b/uaa_test.go @@ -2,7 +2,6 @@ package pivnet import ( "errors" - "net/http" . "github.com/onsi/ginkgo" @@ -19,7 +18,7 @@ var _ = Describe("UAA", func() { BeforeEach(func() { server = ghttp.NewServer() - tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "") + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", ProxyAuthConfig{}) }) AfterEach(func() { @@ -43,7 +42,7 @@ var _ = Describe("UAA", func() { It("passes on the user agent in the request header", func() { userAgent := "my_user_agent" - tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, userAgent) + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, userAgent, ProxyAuthConfig{}) server.AppendHandlers( ghttp.CombineHandlers( ghttp.VerifyHeaderKV("User-Agent", userAgent), @@ -69,7 +68,7 @@ var _ = Describe("UAA", func() { }) It("returns an error without endpoint", func() { - tokenFetcher = NewTokenFetcher("", "some-refresh-token", false, "") + tokenFetcher = NewTokenFetcher("", "some-refresh-token", false, "", ProxyAuthConfig{}) server.AppendHandlers( ghttp.CombineHandlers( ghttp.VerifyRequest("POST", "/authentication/access_tokens"), @@ -83,5 +82,193 @@ var _ = Describe("UAA", func() { }) }) + Context("when proxy authentication is configured", func() { + Context("with Basic authentication", func() { + It("returns an error when proxy URL is empty but auth type is set", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeBasic, + Username: "user", + Password: "pass", + ProxyURL: "", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + _, err := tokenFetcher.GetToken() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("proxy URL is required")) + }) + + It("returns an error when proxy URL is invalid", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeBasic, + Username: "user", + Password: "pass", + ProxyURL: "://invalid-url", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + _, err := tokenFetcher.GetToken() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse proxy URL")) + }) + + It("accepts valid proxy auth config without error", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeBasic, + Username: "proxyuser", + Password: "proxypass", + ProxyURL: "http://proxy.example.com:8080", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + // TokenFetcher should be created successfully with proxy config + Expect(tokenFetcher).NotTo(BeNil()) + Expect(tokenFetcher.ProxyAuthConfig.AuthType).To(Equal(ProxyAuthTypeBasic)) + Expect(tokenFetcher.ProxyAuthConfig.Username).To(Equal("proxyuser")) + }) + + It("accepts empty username and password for Basic auth", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeBasic, + Username: "", + Password: "", + ProxyURL: "http://proxy.example.com:8080", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + Expect(tokenFetcher).NotTo(BeNil()) + Expect(tokenFetcher.ProxyAuthConfig.AuthType).To(Equal(ProxyAuthTypeBasic)) + }) + + It("handles special characters in username and password", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeBasic, + Username: "user@domain.com", + Password: "p@$$w0rd!#%", + ProxyURL: "http://proxy.example.com:8080", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + Expect(tokenFetcher).NotTo(BeNil()) + Expect(tokenFetcher.ProxyAuthConfig.Username).To(Equal("user@domain.com")) + Expect(tokenFetcher.ProxyAuthConfig.Password).To(Equal("p@$$w0rd!#%")) + }) + + It("supports HTTPS proxy URLs", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeBasic, + Username: "proxyuser", + Password: "proxypass", + ProxyURL: "https://secure-proxy.example.com:8443", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + Expect(tokenFetcher).NotTo(BeNil()) + Expect(tokenFetcher.ProxyAuthConfig.ProxyURL).To(Equal("https://secure-proxy.example.com:8443")) + }) + + It("supports proxy URLs with custom ports", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeBasic, + Username: "proxyuser", + Password: "proxypass", + ProxyURL: "http://proxy.example.com:3128", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + Expect(tokenFetcher).NotTo(BeNil()) + Expect(tokenFetcher.ProxyAuthConfig.ProxyURL).To(ContainSubstring(":3128")) + }) + }) + + Context("with SPNEGO authentication", func() { + It("returns an error when username is empty", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeSPNEGO, + Username: "", + Password: "password", + ProxyURL: "http://proxy.example.com:8080", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + _, err := tokenFetcher.GetToken() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("username")) + }) + + It("returns an error when password is empty", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeSPNEGO, + Username: "user@REALM.COM", + Password: "", + ProxyURL: "http://proxy.example.com:8080", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + _, err := tokenFetcher.GetToken() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("password")) + }) + + It("returns an error when proxy URL is empty", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeSPNEGO, + Username: "user@REALM.COM", + Password: "password", + ProxyURL: "", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + _, err := tokenFetcher.GetToken() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("proxy URL is required")) + }) + + It("accepts valid SPNEGO config with Kerberos realm", func() { + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeSPNEGO, + Username: "user@REALM.COM", + Password: "password", + ProxyURL: "http://proxy.example.com:8080", + Krb5Config: "/etc/krb5.conf", + } + tokenFetcher = NewTokenFetcher(server.URL(), "some-refresh-token", false, "", proxyAuthConfig) + + Expect(tokenFetcher).NotTo(BeNil()) + Expect(tokenFetcher.ProxyAuthConfig.AuthType).To(Equal(ProxyAuthTypeSPNEGO)) + Expect(tokenFetcher.ProxyAuthConfig.Krb5Config).To(Equal("/etc/krb5.conf")) + }) + }) + + Context("with different refresh tokens", func() { + It("handles long refresh tokens with proxy auth", func() { + longRefreshToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeBasic, + Username: "proxyuser", + Password: "proxypass", + ProxyURL: "http://proxy.example.com:8080", + } + tokenFetcher = NewTokenFetcher(server.URL(), longRefreshToken, false, "", proxyAuthConfig) + + Expect(tokenFetcher).NotTo(BeNil()) + Expect(tokenFetcher.RefreshToken).To(Equal(longRefreshToken)) + }) + + It("handles short refresh tokens with proxy auth", func() { + shortRefreshToken := "short-token-123" + proxyAuthConfig := ProxyAuthConfig{ + AuthType: ProxyAuthTypeBasic, + Username: "proxyuser", + Password: "proxypass", + ProxyURL: "http://proxy.example.com:8080", + } + tokenFetcher = NewTokenFetcher(server.URL(), shortRefreshToken, false, "", proxyAuthConfig) + + Expect(tokenFetcher).NotTo(BeNil()) + Expect(tokenFetcher.RefreshToken).To(Equal(shortRefreshToken)) + }) + }) + }) }) })