diff --git a/docs/generated/settings/settings-for-tenants.txt b/docs/generated/settings/settings-for-tenants.txt index da550d596f82..c473b2ce6a63 100644 --- a/docs/generated/settings/settings-for-tenants.txt +++ b/docs/generated/settings/settings-for-tenants.txt @@ -117,6 +117,7 @@ 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.timeout duration 15s sets the client timeout for external calls made during JWT authentication (e.g. fetching JWKS, etc.) application server.ldap_authentication.client.tls_certificate string sets the client certificate PEM for establishing mTLS connection with LDAP server application server.ldap_authentication.client.tls_key string sets the client key PEM for establishing mTLS connection with LDAP server application server.ldap_authentication.domain.custom_ca string sets the PEM encoded custom root CA for verifying domain certificates when establishing connection with LDAP server application @@ -127,7 +128,7 @@ server.max_open_transactions_per_gateway integer -1 the maximum number of open S server.oidc_authentication.autologin.enabled (alias: server.oidc_authentication.autologin) boolean false if true, logged-out visitors to the DB Console will be automatically redirected to the OIDC login endpoint application server.oidc_authentication.button_text string Log in with your OIDC provider text to show on button on DB Console login page to login with your OIDC provider (only shown if OIDC is enabled) application server.oidc_authentication.claim_json_key string sets JSON key of principal to extract from payload after OIDC authentication completes (usually email or sid) application -server.oidc_authentication.client.timeout duration 30s sets the client timeout for external calls made during OIDC authentication (e.g. authorization code flow, etc.) application +server.oidc_authentication.client.timeout duration 15s sets the client timeout for external calls made during OIDC authentication (e.g. authorization code flow, etc.) application server.oidc_authentication.client_id string sets OIDC client id application server.oidc_authentication.client_secret string sets OIDC client secret application server.oidc_authentication.enabled boolean false enables or disabled OIDC login for the DB Console application diff --git a/docs/generated/settings/settings.html b/docs/generated/settings/settings.html index 73d7df4c0c74..1a8ce196dace 100644 --- a/docs/generated/settings/settings.html +++ b/docs/generated/settings/settings.html @@ -145,6 +145,7 @@
server.hsts.enabled
booleanfalseif 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 loginServerless/Dedicated/Self-Hosted
server.identity_map.configuration
stringsystem-identity to database-username mappingsServerless/Dedicated/Self-Hosted +
server.jwt_authentication.client.timeout
duration15ssets the client timeout for external calls made during JWT authentication (e.g. fetching JWKS, etc.)Serverless/Dedicated/Self-Hosted
server.ldap_authentication.client.tls_certificate
stringsets the client certificate PEM for establishing mTLS connection with LDAP serverServerless/Dedicated/Self-Hosted
server.ldap_authentication.client.tls_key
stringsets the client key PEM for establishing mTLS connection with LDAP serverServerless/Dedicated/Self-Hosted
server.ldap_authentication.domain.custom_ca
stringsets the PEM encoded custom root CA for verifying domain certificates when establishing connection with LDAP serverServerless/Dedicated/Self-Hosted @@ -155,7 +156,7 @@
server.oidc_authentication.autologin.enabled
(alias: server.oidc_authentication.autologin)
booleanfalseif true, logged-out visitors to the DB Console will be automatically redirected to the OIDC login endpointServerless/Dedicated/Self-Hosted
server.oidc_authentication.button_text
stringLog in with your OIDC providertext to show on button on DB Console login page to login with your OIDC provider (only shown if OIDC is enabled)Serverless/Dedicated/Self-Hosted
server.oidc_authentication.claim_json_key
stringsets JSON key of principal to extract from payload after OIDC authentication completes (usually email or sid)Serverless/Dedicated/Self-Hosted -
server.oidc_authentication.client.timeout
duration30ssets the client timeout for external calls made during OIDC authentication (e.g. authorization code flow, etc.)Serverless/Dedicated/Self-Hosted +
server.oidc_authentication.client.timeout
duration15ssets the client timeout for external calls made during OIDC authentication (e.g. authorization code flow, etc.)Serverless/Dedicated/Self-Hosted
server.oidc_authentication.client_id
stringsets OIDC client idServerless/Dedicated/Self-Hosted
server.oidc_authentication.client_secret
stringsets OIDC client secretServerless/Dedicated/Self-Hosted
server.oidc_authentication.enabled
booleanfalseenables or disabled OIDC login for the DB ConsoleServerless/Dedicated/Self-Hosted diff --git a/pkg/ccl/jwtauthccl/authentication_jwt.go b/pkg/ccl/jwtauthccl/authentication_jwt.go index 697f06e0f839..a3f470b77cb8 100644 --- a/pkg/ccl/jwtauthccl/authentication_jwt.go +++ b/pkg/ccl/jwtauthccl/authentication_jwt.go @@ -87,6 +87,7 @@ func (authenticator *jwtAuthenticator) reloadConfig(ctx context.Context, st *clu func (authenticator *jwtAuthenticator) reloadConfigLocked( ctx context.Context, st *cluster.Settings, ) { + clientTimeout := JWTAuthClientTimeout.Get(&st.SV) conf := jwtAuthenticatorConf{ audience: mustParseValueOrArray(JWTAuthAudience.Get(&st.SV)), enabled: JWTAuthEnabled.Get(&st.SV), @@ -96,8 +97,8 @@ func (authenticator *jwtAuthenticator) reloadConfigLocked( claim: JWTAuthClaim.Get(&st.SV), jwksAutoFetchEnabled: JWKSAutoFetchEnabled.Get(&st.SV), httpClient: httputil.NewClient( - httputil.WithClientTimeout(httputil.StandardHTTPTimeout), - httputil.WithDialerTimeout(httputil.StandardHTTPTimeout), + httputil.WithClientTimeout(clientTimeout), + httputil.WithDialerTimeout(clientTimeout), httputil.WithCustomCAPEM(JWTAuthIssuerCustomCA.Get(&st.SV)), ), } diff --git a/pkg/ccl/jwtauthccl/authentication_jwt_test.go b/pkg/ccl/jwtauthccl/authentication_jwt_test.go index 8b0a367ad252..152977e1aa55 100644 --- a/pkg/ccl/jwtauthccl/authentication_jwt_test.go +++ b/pkg/ccl/jwtauthccl/authentication_jwt_test.go @@ -17,11 +17,13 @@ import ( "crypto/tls" "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" + "regexp" "strings" "testing" "time" @@ -949,3 +951,94 @@ func TestJWTAuthWithCustomCACert(t *testing.T) { }) } } + +func TestJWTAuthClientTimeout(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + + ctx := context.Background() + + // Initiate a test JWKS server locally. + testServer := httptest.NewUnstartedServer(nil) + waitChan := make(chan struct{}, 1) + + mux := http.NewServeMux() + mux.HandleFunc( + "GET /jwks", + func(w http.ResponseWriter, r *http.Request) { + // Hang the request handler to enforce HTTP client timeout. + <-waitChan + }, + ) + + testServer.Config = &http.Server{ + Handler: mux, + } + testServer.Start() + defer func() { + waitChan <- struct{}{} + close(waitChan) + testServer.Close() + }() + + mockGetHttpResponse := func(ctx context.Context, url string, authenticator *jwtAuthenticator) ([]byte, error) { + if strings.Contains(url, "/.well-known/openid-configuration") { + return mockGetHttpResponseWithLocalFileContent(ctx, url, authenticator) + } else if strings.Contains(url, "/oauth2/v3/certs") { + // For fetching JWKS, point to the local test server. + resp, err := authenticator.mu.conf.httpClient.Get( + context.Background(), + testServer.URL+"/jwks", + ) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil + } + return nil, errors.Newf("unsupported route: %s", url) + } + getHttpResponseTestHook := testutils.TestingHook(&getHttpResponse, mockGetHttpResponse) + defer getHttpResponseTestHook() + + s := serverutils.StartServerOnly(t, base.TestServerArgs{}) + defer s.Stopper().Stop(ctx) + + identMapString := "" + identMap, err := identmap.From(strings.NewReader(identMapString)) + require.NoError(t, err) + + // Create a key to sign the token using testdata. + // The same will be fetched through the JWKS URL to verify the token. + keySet := createJWKSFromFile(t, "testdata/www.idp1apis.com_oauth2_v3_certs_private") + key, _ := keySet.Get(0) + validIssuer := "https://accounts.idp1.com" + token := createJWT( + t, username1, audience1, validIssuer, timeutil.Now().Add(time.Hour), key, jwa.RS256, "", "") + + JWTAuthEnabled.Override(ctx, &s.ClusterSettings().SV, true) + JWTAuthIssuers.Override(ctx, &s.ClusterSettings().SV, validIssuer) + JWKSAutoFetchEnabled.Override(ctx, &s.ClusterSettings().SV, true) + JWTAuthAudience.Override(ctx, &s.ClusterSettings().SV, audience1) + JWTAuthClientTimeout.Override(ctx, &s.ClusterSettings().SV, time.Millisecond) + + verifier := ConfigureJWTAuth(ctx, s.AmbientCtx(), s.ClusterSettings(), s.StorageClusterID()) + errMsg, err := verifier.ValidateJWTLogin( + ctx, + s.ClusterSettings(), + username.MakeSQLUsernameFromPreNormalizedString(username1), + token, + identMap, + ) + require.Regexp( + t, + regexp.MustCompile(`unable to fetch jwks:.*\(Client.Timeout exceeded while awaiting headers\)`), + errMsg, + ) + require.ErrorContains(t, err, "JWT authentication: unable to validate token") +} diff --git a/pkg/ccl/jwtauthccl/settings.go b/pkg/ccl/jwtauthccl/settings.go index 2afa4317ac70..26fb0ecf9d3e 100644 --- a/pkg/ccl/jwtauthccl/settings.go +++ b/pkg/ccl/jwtauthccl/settings.go @@ -12,6 +12,7 @@ import ( "bytes" "crypto/x509" "encoding/json" + "time" "github.com/cockroachdb/cockroach/pkg/settings" "github.com/cockroachdb/errors" @@ -28,6 +29,7 @@ const ( JWTAuthClaimSettingName = baseJWTAuthSettingName + "claim" JWKSAutoFetchEnabledSettingName = baseJWTAuthSettingName + "jwks_auto_fetch.enabled" jwtAuthIssuerCustomCASettingName = baseJWTAuthSettingName + "issuers.custom_ca" + jwtAuthClientTimeoutSettingName = baseJWTAuthSettingName + "client.timeout" ) // JWTAuthClaim sets the JWT claim that is parsed to get the username. @@ -98,6 +100,18 @@ var JWKSAutoFetchEnabled = settings.RegisterBoolSetting( settings.WithReportable(true), ) +// JWTAuthClientTimeout is the client timeout for all the external calls made +// during JWT authentication (e.g. fetching JWKS, etc.). +var JWTAuthClientTimeout = settings.RegisterDurationSetting( + settings.ApplicationLevel, + jwtAuthClientTimeoutSettingName, + "sets the client timeout for external calls made during JWT authentication "+ + "(e.g. fetching JWKS, etc.)", + 15*time.Second, + settings.NonNegativeDuration, + settings.WithPublic, +) + func validateJWTAuthIssuers(values *settings.Values, s string) error { var issuers []string diff --git a/pkg/ccl/oidcccl/settings.go b/pkg/ccl/oidcccl/settings.go index 3b13e33cf7f6..b6a9e3f74d68 100644 --- a/pkg/ccl/oidcccl/settings.go +++ b/pkg/ccl/oidcccl/settings.go @@ -81,7 +81,7 @@ var OIDCAuthClientTimeout = settings.RegisterDurationSetting( oidcAuthClientTimeoutSettingName, "sets the client timeout for external calls made during OIDC authentication "+ "(e.g. authorization code flow, etc.)", - 30*time.Second, + 15*time.Second, settings.NonNegativeDuration, settings.WithPublic, )