diff --git a/commonconfig/auth.go b/commonconfig/auth.go index a28674cbf..47f6746a4 100644 --- a/commonconfig/auth.go +++ b/commonconfig/auth.go @@ -2,14 +2,17 @@ package commonconfig import ( "context" + "crypto/tls" "fmt" "github.com/go-playground/validator/v10" + "google.golang.org/grpc/credentials" "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton/provider/authentication" "github.com/smartcontractkit/chainlink-canton/deployment/authentication/authorizationcode" "github.com/smartcontractkit/chainlink-canton/deployment/authentication/clientcredentials" + "github.com/smartcontractkit/chainlink-canton/deployment/authentication/static" ) // Supported authentication types for Canton participant APIs. @@ -33,6 +36,9 @@ type AuthConfig struct { // Defaults to "static" when omitted (backward compatible). Type string `toml:"type" validate:"required,oneof=static insecureStatic clientCredentials authorizationCode"` + // InsecureSkipVerify configures the authentication provider to disable TLS certification validation. + InsecureSkipVerify bool `toml:"insecure_skip_verify,omitempty"` + // UserID is the user ID for the authentication. Required for clientCredentials and authorizationCode only. UserID string `toml:"user_id" validate:"required_if=Type clientCredentials,required_if=Type authorizationCode"` @@ -82,21 +88,42 @@ func (a *AuthConfig) NewProvider(ctx context.Context) (authentication.Provider, return nil, fmt.Errorf("static auth requires a JWT token (set auth.jwt)") } - return authentication.NewStaticProvider(a.JWT), nil + var options []static.ProviderOption + if a.InsecureSkipVerify { + options = append(options, static.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // Intentionally disabling TLS verification opt-in + }))) + } + + return static.NewStaticProvider(a.JWT, options...), nil case AuthTypeClientCredentials: if a.AuthURL == "" || a.ClientID == "" || a.ClientSecret == "" { return nil, fmt.Errorf("clientCredentials auth requires auth_url, client_id, and client_secret") } - return clientcredentials.NewDiscoveryProvider(ctx, a.AuthURL, a.ClientID, a.ClientSecret) + var options []clientcredentials.ProviderOption + if a.InsecureSkipVerify { + options = append(options, clientcredentials.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // Intentionally disabling TLS verification opt-in + }))) + } + + return clientcredentials.NewDiscoveryProvider(ctx, a.AuthURL, a.ClientID, a.ClientSecret, options...) case AuthTypeAuthorizationCode: if a.AuthURL == "" || a.ClientID == "" { return nil, fmt.Errorf("authorizationCode auth requires auth_url and client_id") } - return authorizationcode.NewDiscoveryProvider(ctx, a.AuthURL, a.ClientID) + var options []authorizationcode.ProviderOption + if a.InsecureSkipVerify { + options = append(options, authorizationcode.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // Intentionally disabling TLS verification opt-in + }))) + } + + return authorizationcode.NewDiscoveryProvider(ctx, a.AuthURL, a.ClientID, options...) default: return nil, fmt.Errorf("unsupported auth type: %q (expected static, insecureStatic, clientCredentials, or authorizationCode)", authType) diff --git a/deployment/authentication/static/static.go b/deployment/authentication/static/static.go new file mode 100644 index 000000000..ea1793b05 --- /dev/null +++ b/deployment/authentication/static/static.go @@ -0,0 +1,104 @@ +package static + +import ( + "context" + "crypto/tls" + + "golang.org/x/oauth2" + "google.golang.org/grpc/credentials" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton/provider/authentication" +) + +var _ authentication.Provider = &Provider{} + +type Provider struct { + accessToken string + transportCredentials credentials.TransportCredentials +} + +type staticProviderConfig struct { + transportCredentials credentials.TransportCredentials +} + +func defaultStaticProviderConfig() staticProviderConfig { + return staticProviderConfig{ + transportCredentials: credentials.NewTLS( + &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + ), + } +} + +// ProviderOption configures the client credentials Provider using the functional options pattern. +// Options allow customization of the provider without breaking API compatibility. +type ProviderOption func(*staticProviderConfig) + +// WithTransportCredentials configures the Provider to use the given transport credentials for gRPC connections. +// This allows customization of TLS settings, including certificate verification and minimum TLS version. +// The default transport credentials use TLS 1.2 or higher. +// +// Example: +// +// WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})) +func WithTransportCredentials(credentials credentials.TransportCredentials) ProviderOption { + return func(config *staticProviderConfig) { + config.transportCredentials = credentials + } +} + +func NewStaticProvider(accessToken string, options ...ProviderOption) *Provider { + cfg := defaultStaticProviderConfig() + for _, option := range options { + option(&cfg) + } + + return &Provider{ + accessToken: accessToken, + transportCredentials: cfg.transportCredentials, + } +} + +func (p Provider) TokenSource() oauth2.TokenSource { + return oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: p.accessToken, + }) +} + +func (p Provider) TransportCredentials() credentials.TransportCredentials { + return p.transportCredentials +} + +func (p Provider) PerRPCCredentials() credentials.PerRPCCredentials { + return secureTokenSource{ + TokenSource: p.TokenSource(), + } +} + +// secureTokenSource is a secure OAuth2 PerRPCCredentials implementation that +// requires transport security. +type secureTokenSource struct { + oauth2.TokenSource +} + +var _ credentials.PerRPCCredentials = secureTokenSource{} + +func (ts secureTokenSource) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { + token, err := ts.Token() + if err != nil { + return nil, err + } + if token == nil { + //nolint:nilnil // nothing to do here, just returning no metadata and no error + return nil, nil + } + + return map[string]string{ + "authorization": "Bearer " + token.AccessToken, + }, nil +} + +func (ts secureTokenSource) RequireTransportSecurity() bool { + return true +} diff --git a/deployment/authentication/static/static_test.go b/deployment/authentication/static/static_test.go new file mode 100644 index 000000000..c5eebaa4b --- /dev/null +++ b/deployment/authentication/static/static_test.go @@ -0,0 +1,36 @@ +package static + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/credentials/insecure" +) + +func TestStaticProvider(t *testing.T) { + t.Parallel() + + testToken := "test-token-123" + provider := NewStaticProvider(testToken) + + token, err := provider.TokenSource().Token() + require.NoError(t, err) + assert.Equal(t, testToken, token.AccessToken) + + transportCredentials := provider.TransportCredentials() + require.NotNil(t, transportCredentials) + assert.NotEqual(t, insecure.NewCredentials(), transportCredentials) + + perRPCCredentials := provider.PerRPCCredentials() + require.NotNil(t, perRPCCredentials) + + metadata, err := perRPCCredentials.GetRequestMetadata(t.Context()) + require.NoError(t, err) + header, ok := metadata["authorization"] + require.True(t, ok, "PerRPCCredentials didn't return authorization header") + assert.Equal(t, "Bearer "+testToken, header) + + requireTransportSecurity := perRPCCredentials.RequireTransportSecurity() + assert.True(t, requireTransportSecurity, "PerRPCCredentials must require transport security") +}