Skip to content
Merged
129 changes: 129 additions & 0 deletions flipt-client-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ The `NewClient` constructor accepts a variadic number of `Option` functions that
- `WithReference`: The [reference](https://docs.flipt.io/guides/user/using-references) to use when fetching flag state. If not provided, reference will not be used.
- `WithFetchMode`: The fetch mode to use when fetching flag state. If not provided, the client will default to polling.
- `WithErrorStrategy`: The error strategy to use when fetching flag state. If not provided, the client will default to `Fail`. See the [Error Strategies](#error-strategies) section for more information.
- `WithTLSConfig`: The TLS configuration for connecting to servers with custom certificates. See [TLS Configuration](#tls-configuration). Note: if used with `WithHTTPClient`, this should be called after setting the HTTP client.

### Authentication

Expand All @@ -134,6 +135,134 @@ The `Client` supports the following authentication strategies:
- [Client Token Authentication](https://docs.flipt.io/authentication/using-tokens)
- [JWT Authentication](https://docs.flipt.io/authentication/using-jwts)

### TLS Configuration

The `Client` supports configuring TLS settings for secure connections to Flipt servers using the standard library `tls.Config`. This provides maximum flexibility for:

- Connecting to Flipt servers with self-signed certificates
- Using custom Certificate Authorities (CAs)
- Implementing mutual TLS authentication
- Testing with insecure connections (development only)

#### Basic TLS with Custom CA Certificate

```go
package main

import (
"context"
"crypto/tls"
"crypto/x509"
"log"
"os"

flipt "go.flipt.io/flipt-client"
)

func main() {
ctx := context.Background()

// Load CA certificate
caCert, err := os.ReadFile("/path/to/ca.pem")
if err != nil {
log.Fatal(err)
}

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

tlsConfig := &tls.Config{
RootCAs: caCertPool,
}

client, err := flipt.NewClient(
ctx,
flipt.WithURL("https://flipt.example.com"),
flipt.WithTLSConfig(tlsConfig),
flipt.WithClientTokenAuthentication("your-token"),
)
if err != nil {
log.Fatal(err)
}
defer client.Close(ctx)
}
```

#### Mutual TLS Authentication

```go
// Load client certificate and key
clientCert, err := tls.LoadX509KeyPair("/path/to/client.pem", "/path/to/client.key")
if err != nil {
log.Fatal(err)
}

// Load CA certificate
caCert, err := os.ReadFile("/path/to/ca.pem")
if err != nil {
log.Fatal(err)
}

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

tlsConfig := &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
}

client, err := flipt.NewClient(
ctx,
flipt.WithURL("https://flipt.example.com"),
flipt.WithTLSConfig(tlsConfig),
flipt.WithClientTokenAuthentication("your-token"),
)
```

#### Development Mode (Insecure)

**⚠️ WARNING: Only use this in development environments!**

```go
// Skip certificate verification (NOT for production)
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}

client, err := flipt.NewClient(
ctx,
flipt.WithURL("https://localhost:8443"),
flipt.WithTLSConfig(tlsConfig),
flipt.WithClientTokenAuthentication("your-token"),
)
```

#### Advanced TLS Configuration

Since the client accepts a standard `tls.Config`, you have access to all TLS configuration options:

```go
tlsConfig := &tls.Config{
// Custom CA certificates
RootCAs: caCertPool,

// Client certificates for mutual TLS
Certificates: []tls.Certificate{clientCert},

// Minimum TLS version
MinVersion: tls.VersionTLS12,

// Custom cipher suites
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},

// Server name for SNI
ServerName: "flipt.example.com",
}
```

### Error Strategies

The `Client` supports the following error strategies:
Expand Down
28 changes: 27 additions & 1 deletion flipt-client-go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package flipt_test

import (
"context"
"crypto/tls"
"crypto/x509"
"os"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -34,6 +37,27 @@ func (s *ClientTestSuite) SetupSuite() {
opts = append(opts, flipt.WithClientTokenAuthentication(s.authToken))
}

// Configure TLS if HTTPS URL is provided
if s.fliptURL != "" && strings.HasPrefix(s.fliptURL, "https://") {
caCertPath := os.Getenv("FLIPT_CA_CERT_PATH")
if caCertPath != "" {
caCertData, err := os.ReadFile(caCertPath)
require.NoError(s.T(), err)

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCertData)

opts = append(opts, flipt.WithTLSConfig(&tls.Config{
RootCAs: caCertPool,
}))
} else {
// Fallback to insecure for local testing
opts = append(opts, flipt.WithTLSConfig(&tls.Config{
InsecureSkipVerify: true,
}))
}
}

var err error
s.client, err = flipt.NewClient(context.TODO(), opts...)
require.NoError(s.T(), err)
Expand All @@ -46,7 +70,9 @@ func (s *ClientTestSuite) TearDownSuite() {
func (s *ClientTestSuite) TestInvalidAuthentication() {
_, err := flipt.NewClient(context.TODO(),
flipt.WithURL(s.fliptURL),
flipt.WithClientTokenAuthentication("invalid"))
flipt.WithClientTokenAuthentication("invalid"),
flipt.WithTLSConfig(&tls.Config{InsecureSkipVerify: true}),
)
s.EqualError(err, "failed to fetch initial state: unexpected status code: 401")
}

Expand Down
36 changes: 36 additions & 0 deletions flipt-client-go/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package flipt

import (
"crypto/tls"
"fmt"
"net"
"net/http"
Expand Down Expand Up @@ -91,6 +92,41 @@ func WithRequestTimeout(timeout time.Duration) Option {
}
}

// WithTLSConfig sets the TLS configuration for the client using the standard library tls.Config.
// This provides maximum flexibility for configuring TLS settings including custom CAs,
// mutual TLS authentication, and certificate verification options.
// Note: if used with WithHTTPClient, this should be called after setting the HTTP client.
func WithTLSConfig(tlsConfig *tls.Config) Option {
return func(cfg *config) {
if tlsConfig == nil {
return
}

// Create a new HTTP client based on the one provided or the default one
var (
transport = defaultHTTPClient.Transport.(*http.Transport).Clone()
timeout = defaultHTTPClient.Timeout
)

if cfg.HTTPClient != nil {
timeout = cfg.HTTPClient.Timeout
if cfg.HTTPClient.Transport != nil {
if t, ok := cfg.HTTPClient.Transport.(*http.Transport); ok {
transport = t.Clone()
}
}
}

// Apply the provided TLS config
transport.TLSClientConfig = tlsConfig

cfg.HTTPClient = &http.Client{
Transport: transport,
Timeout: timeout,
}
}
}

// defaultHTTPClient is the default HTTP client used by the client.
// It is used to make requests to the upstream Flipt instance and is configured to be best compatible with streaming mode.
var defaultHTTPClient = &http.Client{
Expand Down
10 changes: 6 additions & 4 deletions test/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ var (
}

goVersions = []containerConfig{
{base: "golang:1.23-bookworm", setup: []string{"apt-get update", "apt-get install -y build-essential"}},
{base: "golang:1.23-bullseye", setup: []string{"apt-get update", "apt-get install -y build-essential"}},
{base: "golang:1.23-alpine", setup: []string{"apk update", "apk add --no-cache build-base"}},
{base: "golang:1.23-bookworm", setup: []string{"apt-get update", "apt-get install -y build-essential"}, useHTTPS: true},
{base: "golang:1.23-bullseye", setup: []string{"apt-get update", "apt-get install -y build-essential"}, useHTTPS: true},
{base: "golang:1.23-alpine", setup: []string{"apk update", "apk add --no-cache build-base"}, useHTTPS: true},
}

rubyVersions = []containerConfig{
Expand Down Expand Up @@ -431,8 +431,10 @@ func goTests(ctx context.Context, root *dagger.Container, t *testCase) error {
WithWorkdir("/src").
WithDirectory("/src", t.hostDir.Directory("flipt-client-go")).
WithFile("/src/ext/flipt_engine_wasm.wasm", t.engine.File(wasmFile)).
WithDirectory("/src/test/fixtures/tls", t.hostDir.Directory("test/fixtures/tls")).
WithServiceBinding("flipt", t.flipt.AsService()).
WithEnvVariable("FLIPT_URL", "http://flipt:8080").
WithEnvVariable("FLIPT_URL", "https://flipt:8443").
WithEnvVariable("FLIPT_CA_CERT_PATH", "/src/test/fixtures/tls/ca.crt").
WithEnvVariable("FLIPT_AUTH_TOKEN", "secret").
WithExec(args("go mod download")).
WithExec(args("go test -v -timeout 30s ./...")).
Expand Down
Loading