diff --git a/docs/generated/settings/settings-for-tenants.txt b/docs/generated/settings/settings-for-tenants.txt
index 01928db040a5..3bd7545140b4 100644
--- a/docs/generated/settings/settings-for-tenants.txt
+++ b/docs/generated/settings/settings-for-tenants.txt
@@ -88,6 +88,8 @@ server.hot_ranges_request.node.timeout duration 5m0s the duration allowed for a
server.hsts.enabled boolean false if true, HSTS headers will be sent along with all HTTP requests. The headers will contain a max-age setting of one year. Browsers honoring the header will always use HTTPS to access the DB Console. Ensure that TLS is correctly configured prior to enabling. application
server.http.base_path string / path to redirect the user to upon succcessful login application
server.identity_map.configuration string system-identity to database-username mappings application
+server.jwt_authentication.client.custom_ca string sets the custom root CA (appended to system's default CAs) for verifying certificates when interacting with jwt internal HTTPS client application
+server.jwt_authentication.client.timeout duration 3s sets the timeout for jwt http client operations application
server.log_gc.max_deletions_per_cycle integer 1000 the maximum number of entries to delete on each purge of log-like system tables application
server.log_gc.period duration 1h0m0s the period at which log-like system tables are checked for old entries application
server.max_connections_per_gateway integer -1 the maximum number of SQL connections per gateway allowed at a given time (note: this will only limit future connection attempts and will not affect already established connections). Negative values result in unlimited number of connections. Superusers are not affected by this limit. application
diff --git a/docs/generated/settings/settings.html b/docs/generated/settings/settings.html
index e29897bde112..f68888365606 100644
--- a/docs/generated/settings/settings.html
+++ b/docs/generated/settings/settings.html
@@ -116,6 +116,8 @@
server.hsts.enabled
| boolean | false | if true, HSTS headers will be sent along with all HTTP requests. The headers will contain a max-age setting of one year. Browsers honoring the header will always use HTTPS to access the DB Console. Ensure that TLS is correctly configured prior to enabling. | Serverless/Dedicated/Self-Hosted |
server.http.base_path
| string | / | path to redirect the user to upon succcessful login | Serverless/Dedicated/Self-Hosted |
server.identity_map.configuration
| string |
| system-identity to database-username mappings | Serverless/Dedicated/Self-Hosted |
+server.jwt_authentication.client.custom_ca
| string |
| sets the custom root CA (appended to system's default CAs) for verifying certificates when interacting with jwt internal HTTPS client | Serverless/Dedicated/Self-Hosted |
+server.jwt_authentication.client.timeout
| duration | 3s | sets the timeout for jwt http client operations | Serverless/Dedicated/Self-Hosted |
server.log_gc.max_deletions_per_cycle
| integer | 1000 | the maximum number of entries to delete on each purge of log-like system tables | Serverless/Dedicated/Self-Hosted |
server.log_gc.period
| duration | 1h0m0s | the period at which log-like system tables are checked for old entries | Serverless/Dedicated/Self-Hosted |
server.max_connections_per_gateway
| integer | -1 | the maximum number of SQL connections per gateway allowed at a given time (note: this will only limit future connection attempts and will not affect already established connections). Negative values result in unlimited number of connections. Superusers are not affected by this limit. | Serverless/Dedicated/Self-Hosted |
diff --git a/pkg/ccl/jwtauthccl/authentication_jwt.go b/pkg/ccl/jwtauthccl/authentication_jwt.go
index 8a106b46742e..0308e1d62827 100644
--- a/pkg/ccl/jwtauthccl/authentication_jwt.go
+++ b/pkg/ccl/jwtauthccl/authentication_jwt.go
@@ -14,6 +14,7 @@ import (
"fmt"
"io"
"strings"
+ "time"
"github.com/cockroachdb/cockroach/pkg/ccl/utilccl"
"github.com/cockroachdb/cockroach/pkg/security/username"
@@ -71,6 +72,8 @@ type jwtAuthenticatorConf struct {
jwks jwk.Set
claim string
jwksAutoFetchEnabled bool
+ clientTimeout time.Duration
+ customCA string
}
// reloadConfig locks mutex and then refreshes the values in conf from the cluster settings.
@@ -91,6 +94,8 @@ func (authenticator *jwtAuthenticator) reloadConfigLocked(
jwks: mustParseJWKS(JWTAuthJWKS.Get(&st.SV)),
claim: JWTAuthClaim.Get(&st.SV),
jwksAutoFetchEnabled: JWKSAutoFetchEnabled.Get(&st.SV),
+ clientTimeout: JWTClientTimeout.Get(&st.SV),
+ customCA: JWTClientCustomCA.Get(&st.SV),
}
if !authenticator.mu.conf.enabled && conf.enabled {
@@ -146,7 +151,8 @@ func (authenticator *jwtAuthenticator) ValidateJWTLogin(
// The token will be parsed again later to actually verify the signature.
unverifiedToken, err := jwt.Parse(tokenBytes)
if err != nil {
- return errors.Newf("JWT authentication: invalid token")
+ return errors.WithDetailf(
+ errors.Newf("JWT authentication: invalid token"), "token parsing failed: %v", err)
}
// Check for issuer match against configured issuers.
@@ -168,9 +174,11 @@ func (authenticator *jwtAuthenticator) ValidateJWTLogin(
var jwkSet jwk.Set
// If auto-fetch is enabled, fetch the JWKS remotely from the issuer's well known jwks url.
if authenticator.mu.conf.jwksAutoFetchEnabled {
- jwkSet, err = remoteFetchJWKS(ctx, issuerUrl)
+ jwkSet, err = authenticator.remoteFetchJWKS(ctx, issuerUrl)
if err != nil {
- return errors.Newf("JWT authentication: unable to validate token")
+ return errors.WithDetailf(
+ errors.Newf("JWT authentication: unable to validate token"),
+ "unable to fetch jwks: %v", err)
}
} else {
jwkSet = authenticator.mu.conf.jwks
@@ -179,7 +187,7 @@ func (authenticator *jwtAuthenticator) ValidateJWTLogin(
// Now that both the issuer and key-id are matched, parse the token again to validate the signature.
parsedToken, err := jwt.Parse(tokenBytes, jwt.WithKeySet(jwkSet), jwt.WithValidate(true), jwt.InferAlgorithmFromKey(true))
if err != nil {
- return errors.Newf("JWT authentication: invalid token")
+ return errors.WithDetailf(errors.Newf("JWT authentication: invalid token"), "unable to parse token: %v", err)
}
// Extract all requested principals from the token. By default, we take it from the subject unless they specify
@@ -269,12 +277,14 @@ func (authenticator *jwtAuthenticator) ValidateJWTLogin(
}
// remoteFetchJWKS fetches the JWKS from the provided URI.
-func remoteFetchJWKS(ctx context.Context, issuerUrl string) (jwk.Set, error) {
- jwksUrl, err := getJWKSUrl(ctx, issuerUrl)
+func (authenticator *jwtAuthenticator) remoteFetchJWKS(
+ ctx context.Context, issuerUrl string,
+) (jwk.Set, error) {
+ jwksUrl, err := authenticator.getJWKSUrl(ctx, issuerUrl)
if err != nil {
return nil, err
}
- body, err := getHttpResponse(ctx, jwksUrl)
+ body, err := getHttpResponse(ctx, jwksUrl, authenticator)
if err != nil {
return nil, err
}
@@ -286,12 +296,14 @@ func remoteFetchJWKS(ctx context.Context, issuerUrl string) (jwk.Set, error) {
}
// getJWKSUrl returns the JWKS URI from the OpenID configuration endpoint.
-func getJWKSUrl(ctx context.Context, issuerUrl string) (string, error) {
+func (authenticator *jwtAuthenticator) getJWKSUrl(
+ ctx context.Context, issuerUrl string,
+) (string, error) {
type OIDCConfigResponse struct {
JWKSUri string `json:"jwks_uri"`
}
openIdConfigEndpoint := getOpenIdConfigEndpoint(issuerUrl)
- body, err := getHttpResponse(ctx, openIdConfigEndpoint)
+ body, err := getHttpResponse(ctx, openIdConfigEndpoint, authenticator)
if err != nil {
return "", err
}
@@ -311,8 +323,11 @@ func getOpenIdConfigEndpoint(issuerUrl string) string {
return openIdConfigEndpoint
}
-var getHttpResponse = func(ctx context.Context, url string) ([]byte, error) {
- resp, err := httputil.Get(ctx, url)
+var getHttpResponse = func(ctx context.Context, url string, authenticator *jwtAuthenticator) ([]byte, error) {
+ responseTimeout := authenticator.mu.conf.clientTimeout
+ httpClient := httputil.NewClientWithTimeoutsCustomCA(
+ responseTimeout, responseTimeout, authenticator.mu.conf.customCA)
+ resp, err := httpClient.Get(context.Background(), url)
if err != nil {
return nil, err
}
@@ -354,6 +369,12 @@ var ConfigureJWTAuth = func(
JWKSAutoFetchEnabled.SetOnChange(&st.SV, func(ctx context.Context) {
authenticator.reloadConfig(ambientCtx.AnnotateCtx(ctx), st)
})
+ JWTClientTimeout.SetOnChange(&st.SV, func(ctx context.Context) {
+ authenticator.reloadConfig(ambientCtx.AnnotateCtx(ctx), st)
+ })
+ JWTClientCustomCA.SetOnChange(&st.SV, func(ctx context.Context) {
+ authenticator.reloadConfig(ambientCtx.AnnotateCtx(ctx), st)
+ })
return &authenticator
}
diff --git a/pkg/ccl/jwtauthccl/authentication_jwt_test.go b/pkg/ccl/jwtauthccl/authentication_jwt_test.go
index ce0d97713003..511f10656f66 100644
--- a/pkg/ccl/jwtauthccl/authentication_jwt_test.go
+++ b/pkg/ccl/jwtauthccl/authentication_jwt_test.go
@@ -618,7 +618,7 @@ func TestAudienceCheck(t *testing.T) {
// mockGetHttpResponseWithLocalFileContent is a mock function for getHttpResponse. This is used to intercept the call to
// getHttpResponse and return the content of a local file instead of making a http call.
-var mockGetHttpResponseWithLocalFileContent = func(ctx context.Context, url string) ([]byte, error) {
+var mockGetHttpResponseWithLocalFileContent = func(ctx context.Context, url string, authenticator *jwtAuthenticator) ([]byte, error) {
// remove https:// and replace / with _ in the url to get the testdata file name
fileName := "testdata/" + strings.ReplaceAll(strings.ReplaceAll(url, "https://", ""), "/", "_")
// read content of the file as a byte array
diff --git a/pkg/ccl/jwtauthccl/settings.go b/pkg/ccl/jwtauthccl/settings.go
index 995415fcf663..a8984673e1a1 100644
--- a/pkg/ccl/jwtauthccl/settings.go
+++ b/pkg/ccl/jwtauthccl/settings.go
@@ -13,19 +13,22 @@ import (
"encoding/json"
"github.com/cockroachdb/cockroach/pkg/settings"
+ "github.com/cockroachdb/cockroach/pkg/util/httputil"
"github.com/cockroachdb/errors"
"github.com/lestrrat-go/jwx/jwk"
)
// All cluster settings necessary for the JWT authentication feature.
const (
- baseJWTAuthSettingName = "server.jwt_authentication."
- JWTAuthAudienceSettingName = baseJWTAuthSettingName + "audience"
- JWTAuthEnabledSettingName = baseJWTAuthSettingName + "enabled"
- JWTAuthIssuersSettingName = baseJWTAuthSettingName + "issuers"
- JWTAuthJWKSSettingName = baseJWTAuthSettingName + "jwks"
- JWTAuthClaimSettingName = baseJWTAuthSettingName + "claim"
- JWKSAutoFetchEnabledSettingName = baseJWTAuthSettingName + "jwks_auto_fetch.enabled"
+ baseJWTAuthSettingName = "server.jwt_authentication."
+ JWTAuthAudienceSettingName = baseJWTAuthSettingName + "audience"
+ JWTAuthEnabledSettingName = baseJWTAuthSettingName + "enabled"
+ JWTAuthIssuersSettingName = baseJWTAuthSettingName + "issuers"
+ JWTAuthJWKSSettingName = baseJWTAuthSettingName + "jwks"
+ JWTAuthClaimSettingName = baseJWTAuthSettingName + "claim"
+ JWKSAutoFetchEnabledSettingName = baseJWTAuthSettingName + "jwks_auto_fetch.enabled"
+ JWTAuthClientTimeoutSettingName = baseJWTAuthSettingName + "client.timeout"
+ JWTAuthClientCustomCASettingName = baseJWTAuthSettingName + "client.custom_ca"
)
// JWTAuthClaim sets the JWT claim that is parsed to get the username.
@@ -84,6 +87,22 @@ var JWKSAutoFetchEnabled = settings.RegisterBoolSetting(
settings.WithReportable(true),
)
+// JWTClientTimeout is a cluster setting used for setting jwt http client interactions.
+var JWTClientTimeout = settings.RegisterDurationSetting(
+ settings.ApplicationLevel,
+ JWTAuthClientTimeoutSettingName,
+ "sets the timeout for jwt http client operations",
+ httputil.StandardHTTPTimeout,
+ settings.WithPublic)
+
+var JWTClientCustomCA = settings.RegisterStringSetting(
+ settings.ApplicationLevel,
+ JWTAuthClientCustomCASettingName,
+ "sets the custom root CA (appended to system's default CAs) for verifying "+
+ "certificates when interacting with jwt internal HTTPS client",
+ "",
+ settings.WithPublic)
+
func validateJWTAuthIssuers(values *settings.Values, s string) error {
var issuers []string
diff --git a/pkg/sql/pgwire/auth.go b/pkg/sql/pgwire/auth.go
index 224cede13ce2..8369298ac958 100644
--- a/pkg/sql/pgwire/auth.go
+++ b/pkg/sql/pgwire/auth.go
@@ -553,7 +553,8 @@ func (p *authPipe) LogAuthFailed(
p.loggedFailure = true
var errStr redact.RedactableString
if detailedErr != nil {
- errStr = redact.Sprint(detailedErr)
+ // should we apply redact here?
+ errStr = redact.Sprint(detailedErr).Redact()
}
ev := &eventpb.ClientAuthenticationFailed{
CommonConnectionDetails: p.connDetails,
diff --git a/pkg/sql/pgwire/auth_methods.go b/pkg/sql/pgwire/auth_methods.go
index b2a298b8ea88..b3237b10669d 100644
--- a/pkg/sql/pgwire/auth_methods.go
+++ b/pkg/sql/pgwire/auth_methods.go
@@ -781,7 +781,10 @@ func authJwtToken(
return security.NewErrPasswordUserAuthFailed(user)
}
if err = jwtVerifier.ValidateJWTLogin(ctx, execCfg.Settings, user, []byte(token), identMap); err != nil {
+ // We are logging unsafe part of error also. Also, should we look into
+ // obtaining more specific auth failure reason here?
c.LogAuthFailed(ctx, eventpb.AuthFailReason_CREDENTIALS_INVALID, err)
+ // we are returning unsafe error to client
return err
}
c.LogAuthOK(ctx)
diff --git a/pkg/util/httputil/client.go b/pkg/util/httputil/client.go
index bbeebc3ef200..f1fa79cbc3b2 100644
--- a/pkg/util/httputil/client.go
+++ b/pkg/util/httputil/client.go
@@ -12,6 +12,8 @@ package httputil
import (
"context"
+ "crypto/tls"
+ "crypto/x509"
"io"
"net"
"net/http"
@@ -32,6 +34,25 @@ func NewClientWithTimeout(timeout time.Duration) *Client {
// NewClientWithTimeouts defines a http.Client with the given dialer and client timeouts.
func NewClientWithTimeouts(dialerTimeout, clientTimeout time.Duration) *Client {
+ return NewClientWithTimeoutsCustomCA(dialerTimeout, clientTimeout, "")
+}
+
+// NewClientWithTimeoutsCustomCA defines a http.Client with the given dialer and client timeouts and custom CA pem.
+func NewClientWithTimeoutsCustomCA(
+ dialerTimeout, clientTimeout time.Duration, customCAPem string,
+) *Client {
+ var tlsConf *tls.Config
+ if customCAPem != "" {
+ roots, err := x509.SystemCertPool()
+ if err != nil {
+ return nil
+ }
+ if !roots.AppendCertsFromPEM([]byte(customCAPem)) {
+ return nil
+ }
+ tlsConf = &tls.Config{RootCAs: roots}
+ }
+ t := http.DefaultTransport.(*http.Transport)
return &Client{&http.Client{
Timeout: clientTimeout,
Transport: &http.Transport{
@@ -39,6 +60,15 @@ func NewClientWithTimeouts(dialerTimeout, clientTimeout time.Duration) *Client {
// much higher than on linux).
DialContext: (&net.Dialer{Timeout: dialerTimeout}).DialContext,
DisableKeepAlives: true,
+
+ Proxy: t.Proxy,
+ MaxIdleConns: t.MaxIdleConns,
+ IdleConnTimeout: t.IdleConnTimeout,
+ TLSHandshakeTimeout: t.TLSHandshakeTimeout,
+ ExpectContinueTimeout: t.ExpectContinueTimeout,
+
+ // Add our custom CA.
+ TLSClientConfig: tlsConf,
},
}}
}