Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Improve http.Client usage for security and performance #1910

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions sdk/auth/oauth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package oauth

import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
Expand All @@ -17,15 +16,16 @@ import (
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/opentdf/platform/sdk/httputil"
)

const (
tokenExpirationBuffer = 10 * time.Second
)

type CertExchangeInfo struct {
TLSConfig *tls.Config
Audience []string
HTTPClient *http.Client
Audience []string
}

type ClientCredentials struct {
Expand Down Expand Up @@ -146,6 +146,9 @@ func GetAccessToken(client *http.Client, tokenEndpoint string, scopes []string,
if err != nil {
return nil, err
}
if client == nil {
client = httputil.SafeHTTPClient()
}

resp, err := client.Do(req)
if err != nil {
Expand Down Expand Up @@ -249,6 +252,9 @@ func DoTokenExchange(ctx context.Context, client *http.Client, tokenEndpoint str
if err != nil {
return nil, err
}
if client == nil {
client = httputil.SafeHTTPClient()
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("error making request to IdP for token exchange: %w", err)
Expand Down Expand Up @@ -313,12 +319,11 @@ func DoCertExchange(ctx context.Context, tokenEndpoint string, exchangeInfo Cert
if err != nil {
return nil, err
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: exchangeInfo.TLSConfig,
},
}

client := exchangeInfo.HTTPClient
if client == nil {
client = httputil.SafeHTTPClient()
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("error making request to IdP for certificate exchange: %w", err)
Expand Down
8 changes: 6 additions & 2 deletions sdk/auth/oauth/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/opentdf/platform/lib/fixtures"
"github.com/opentdf/platform/sdk/httputil"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
tc "github.com/testcontainers/testcontainers-go"
Expand Down Expand Up @@ -78,12 +79,15 @@ func (s *OAuthSuite) TestCertExchangeFromKeycloak() {
rootCAs, _ := x509.SystemCertPool()
rootCAs.AppendCertsFromPEM(ca)
s.Require().NoError(err)
tlsConfig := tls.Config{
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
RootCAs: rootCAs,
}
exhcangeInfo := CertExchangeInfo{TLSConfig: &tlsConfig, Audience: []string{"opentdf-sdk"}}
exhcangeInfo := CertExchangeInfo{
HTTPClient: httputil.SafeHTTPClientWithTLSConfig(tlsConfig),
Audience: []string{"opentdf-sdk"},
}

tok, err := DoCertExchange(
context.Background(),
Expand Down
19 changes: 11 additions & 8 deletions sdk/auth/token_adding_interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/opentdf/platform/sdk/httputil"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
Expand All @@ -25,15 +26,22 @@ const (
)

func NewTokenAddingInterceptor(t AccessTokenSource, c *tls.Config) TokenAddingInterceptor {
return NewTokenAddingInterceptorWithClient(t, httputil.SafeHTTPClientWithTLSConfig(c))
}

func NewTokenAddingInterceptorWithClient(t AccessTokenSource, c *http.Client) TokenAddingInterceptor {
if c == nil {
c = httputil.SafeHTTPClient()
}
return TokenAddingInterceptor{
tokenSource: t,
tlsConfig: c,
httpClient: c,
}
}

type TokenAddingInterceptor struct {
tokenSource AccessTokenSource
tlsConfig *tls.Config
httpClient *http.Client
}

func (i TokenAddingInterceptor) AddCredentials(
Expand All @@ -45,12 +53,7 @@ func (i TokenAddingInterceptor) AddCredentials(
opts ...grpc.CallOption,
) error {
newMetadata := make([]string, 0)
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: i.tlsConfig,
},
}
accessToken, err := i.tokenSource.AccessToken(ctx, client)
accessToken, err := i.tokenSource.AccessToken(ctx, i.httpClient)
if err == nil {
newMetadata = append(newMetadata, "Authorization", fmt.Sprintf("DPoP %s", accessToken))
} else {
Expand Down
64 changes: 64 additions & 0 deletions sdk/httputil/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package httputil

import (
"crypto/tls"
"net/http"
"time"
)

const (
defaultTimeout = 120 * time.Second
// defaults to match DefaultTransport - defined to satisfy lint
maxIdleConns = 100
idleConnTimeout = 90 * time.Second
tlsHandshakeTimeout = 10 * time.Second
expectContinueTimeout = 1 * time.Second
)

var preventRedirectCheck = func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse // Prevent following redirects
jentfoo marked this conversation as resolved.
Show resolved Hide resolved
}

var safeHTTPClient = &http.Client{
Transport: http.DefaultTransport,
Timeout: defaultTimeout,
CheckRedirect: preventRedirectCheck,
}

// SafeHTTPClient returns a default http client which has sensible timeouts, won't follow redirects, and enables idle
// connection pooling.
func SafeHTTPClient() *http.Client {
return safeHTTPClient
}

// SafeHTTPClientWithTLSConfig returns a http client which has sensible timeouts, won't follow redirects, and if
// specified a http.Transport with the tls.Config provided.
func SafeHTTPClientWithTLSConfig(cfg *tls.Config) *http.Client {
if cfg == nil {
return safeHTTPClient
}
return SafeHTTPClientWithTransport(&http.Transport{
TLSClientConfig: cfg,
// config below matches DefaultTransport
Proxy: http.ProxyFromEnvironment,
ForceAttemptHTTP2: true,
MaxIdleConns: maxIdleConns,
IdleConnTimeout: idleConnTimeout,
TLSHandshakeTimeout: tlsHandshakeTimeout,
ExpectContinueTimeout: expectContinueTimeout,
})
}

// SafeHTTPClientWithTransport returns a http client which has sensible timeouts, won't follow redirects, and if
// specified the provided http.Transport.
func SafeHTTPClientWithTransport(transport *http.Transport) *http.Client {
if transport == nil {
return safeHTTPClient
}
return &http.Client{
Transport: transport,
// config below matches our values for safeHttpClient
Timeout: defaultTimeout,
CheckRedirect: preventRedirectCheck,
}
}
10 changes: 6 additions & 4 deletions sdk/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package sdk
import (
"crypto/rsa"
"crypto/tls"
"net/http"

"github.com/opentdf/platform/lib/ocrypto"
"github.com/opentdf/platform/sdk/auth"
"github.com/opentdf/platform/sdk/auth/oauth"
"github.com/opentdf/platform/sdk/httputil"
"golang.org/x/oauth2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
Expand All @@ -20,7 +22,7 @@ type config struct {
// Platform configuration structure is subject to change. Consume via accessor methods.
PlatformConfiguration PlatformConfiguration
dialOption grpc.DialOption
tlsConfig *tls.Config
httpClient *http.Client
clientCredentials *oauth.ClientCredentials
tokenExchange *oauth.TokenExchangeInfo
tokenEndpoint string
Expand Down Expand Up @@ -66,7 +68,7 @@ func WithInsecureSkipVerifyConn() Option {
}
c.dialOption = grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))
// used by http client
c.tlsConfig = tlsConfig
c.httpClient = httputil.SafeHTTPClientWithTLSConfig(tlsConfig)
}
}

Expand All @@ -83,7 +85,7 @@ func WithInsecurePlaintextConn() Option {
c.dialOption = grpc.WithTransportCredentials(insecure.NewCredentials())
// used by http client
// FIXME anything to do here
c.tlsConfig = &tls.Config{}
c.httpClient = httputil.SafeHTTPClient()
}
}

Expand All @@ -97,7 +99,7 @@ func WithClientCredentials(clientID, clientSecret string, scopes []string) Optio

func WithTLSCredentials(tls *tls.Config, audience []string) Option {
return func(c *config) {
c.certExchange = &oauth.CertExchangeInfo{TLSConfig: tls, Audience: audience}
c.certExchange = &oauth.CertExchangeInfo{HTTPClient: httputil.SafeHTTPClientWithTLSConfig(tls), Audience: audience}
}
}

Expand Down
13 changes: 6 additions & 7 deletions sdk/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/opentdf/platform/protocol/go/wellknownconfiguration"
"github.com/opentdf/platform/sdk/audit"
"github.com/opentdf/platform/sdk/auth"
"github.com/opentdf/platform/sdk/httputil"
"github.com/opentdf/platform/sdk/internal/archive"
"github.com/xeipuuv/gojsonschema"
"google.golang.org/grpc"
Expand Down Expand Up @@ -154,7 +155,7 @@ func New(platformEndpoint string, opts ...Option) (*SDK, error) {
return nil, err
}
if accessTokenSource != nil {
interceptor := auth.NewTokenAddingInterceptor(accessTokenSource, cfg.tlsConfig)
interceptor := auth.NewTokenAddingInterceptorWithClient(accessTokenSource, cfg.httpClient)
uci = append(uci, interceptor.AddCredentials)
}

Expand Down Expand Up @@ -452,13 +453,11 @@ func getTokenEndpoint(c config) (string, error) {
return "", err
}

httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: c.tlsConfig,
},
client := c.httpClient
if client == nil {
client = httputil.SafeHTTPClient()
}

resp, err := httpClient.Do(req)
resp, err := client.Do(req)
if err != nil {
return "", err
}
Expand Down
Loading