From 6d603997a88a297784614504d87ab5d7ea787243 Mon Sep 17 00:00:00 2001 From: UnicornChance Date: Wed, 10 Sep 2025 21:08:00 +0100 Subject: [PATCH 1/9] Add OCSP retry configuration and HTTP retry mechanism Implements a configurable retry mechanism for OCSP certificate validation with UI configuration in the X.509 client certificate authenticator. Also adds a general-purpose HTTP retry mechanism with exponential backoff and jitter that can be used throughout the codebase. The implementation is opt-in by default (0 retries) to maintain backward compatibility while allowing configuration when needed. Closes #42401 Signed-off-by: UnicornChance cleanup Signed-off-by: UnicornChance cleanup Signed-off-by: UnicornChance cleanup Signed-off-by: UnicornChance cleanup Signed-off-by: UnicornChance --- .../images/x509-ocsp-retry-settings.png | Bin 0 -> 138019 bytes .../topics/authentication/x509.adoc | 16 + .../httpclient/HttpClientProvider.java | 44 +++ .../connections/httpclient/RetryConfig.java | 340 ++++++++++++++++ .../java/org/keycloak/utils/OCSPProvider.java | 17 +- .../httpclient/RetryConfigTest.java | 173 +++++++++ ...actX509ClientCertificateAuthenticator.java | 13 +- ...ClientCertificateAuthenticatorFactory.java | 17 +- .../x509/CertificateValidator.java | 64 ++- .../x509/X509AuthenticatorConfigModel.java | 20 + .../httpclient/DefaultHttpClientFactory.java | 366 ++++++++++++++---- .../DefaultHttpClientFactoryTest.java | 97 ++++- .../testsuite/x509/X509OCSPResponderTest.java | 36 ++ 13 files changed, 1100 insertions(+), 103 deletions(-) create mode 100644 docs/documentation/server_admin/images/x509-ocsp-retry-settings.png create mode 100644 server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java create mode 100644 server-spi-private/src/test/java/org/keycloak/connections/httpclient/RetryConfigTest.java diff --git a/docs/documentation/server_admin/images/x509-ocsp-retry-settings.png b/docs/documentation/server_admin/images/x509-ocsp-retry-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..88d632d3c2ca3b671c7cfe5e5d12ada77d4b2f73 GIT binary patch literal 138019 zcmeFYhdW$d*Eoz6i3Eu#5kv`LMwvv7P8enM(R=hx7@Z+Ph!TR)TY@nLqjymv5scnD ziQYvSy?m4VDewJ0zvuf8-f^Al?6cQeyREhNUQM{Vsyr#lBN74v0#Zcle(LWwVk6C0f9nzf-bS1W+!!~A(Z+0Em;1s z3Mt4f?t^G1AoJ~ekf__zZP8rCKPSQ0yCRCAlv5-f<}$_}RvMe|vQODXksIc^vmpfa zo1AlLJ87a<7c1S8E9s&~y$l4a#{Ou2nvv^FYH^@Q(nA{+RaLWCUjo{P-y(>3O_`G1 zvU9!>a(&yH>)LQ2(5OygKp0$YUX_5AIO6UQNM$pt(qM1++`GC(Fbn*M-6LdzFuO=_ znihQDU>c2;CT1?ym{+E9)}D8#YIY7_8vam{9#Z2B> zG)m@c3Gt2-6N(o4-cE8tXm*e8Wjb@`hr5jB0PHAgXjObTdcTuBjJ%zN^2YT7^Y%^_#h2+3?YP{N%d +6;an-;T#e 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 literal 0 HcmV?d00001 diff --git a/docs/documentation/server_admin/topics/authentication/x509.adoc b/docs/documentation/server_admin/topics/authentication/x509.adoc index f2a4ae0da14b..e1f4c755b8cc 100644 --- a/docs/documentation/server_admin/topics/authentication/x509.adoc +++ b/docs/documentation/server_admin/topics/authentication/x509.adoc @@ -143,6 +143,22 @@ Checks the certificate revocation status by using Online Certificate Status Prot *OCSP Fail-Open Behavior*:: By default the OCSP check must return a positive response in order to continue with a successful authentication. Sometimes however this check can be inconclusive: for example, the OCSP server could be unreachable, overloaded, or the client certificate may not contain an OCSP responder URI. When this setting is turned ON, authentication will be denied only if an explicit negative response is received by the OCSP responder and the certificate is definitely revoked. If a valid OCSP response is not available the authentication attempt will be accepted. +*OCSP Max Retries*:: +Maximum number of retry attempts for OCSP requests that fail due to network issues. Default is 0 (no retries). Setting this to a value greater than 0 enables retry behavior, which can improve reliability when connecting to OCSP responders over unreliable networks. + +*OCSP Timeout (ms)*:: +Timeout in milliseconds for OCSP requests. This applies to both connection and socket timeouts. Default is 10000 ms (10 seconds). Increase this value if the OCSP responder is slow to respond or decrease it if you need faster failure detection. + +==== OCSP Retry Settings + +. Click *Authentication* in the menu. +. Select Flow. +. Select "X509/Validate Username" or "X509/Validate Username Form" execution settings. +. Configure OCSP Retry Settings. +. Click *Save*. ++ +image:images/x509-ocsp-retry-settings.png[OCSP Retry Settings] + *OCSP Responder URI*:: Override the value of the OCSP responder URI in the certificate. diff --git a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java index c0169c776aff..942e5f62dd89 100755 --- a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java @@ -40,6 +40,34 @@ public interface HttpClientProvider extends Provider { */ CloseableHttpClient getHttpClient(); + /** + * Returns a {@code CloseableHttpClient} with default retry behavior. + * The default retry behavior is configured in the factory. + *

+ * The returned {@code HttpClient} instance must never be {@code close()}d by + * the caller. + *

+ * + * @return A CloseableHttpClient with retry capabilities + */ + default CloseableHttpClient getRetriableHttpClient() { + return getHttpClient(); // Default implementation for backward compatibility + } + + /** + * Returns a {@code CloseableHttpClient} with custom retry behavior. + *

+ * The returned {@code HttpClient} instance must never be {@code close()}d by + * the caller. + *

+ * + * @param retryConfig Configuration for retry behavior + * @return A CloseableHttpClient with retry capabilities + */ + default CloseableHttpClient getRetriableHttpClient(RetryConfig retryConfig) { + return getHttpClient(); + } + /** * Helper method * @@ -103,4 +131,20 @@ default long getMaxConsumedResponseSize() { return DEFAULT_MAX_CONSUMED_RESPONSE_SIZE; } + /** + * Sets a custom retry configuration to be used for subsequent calls to + * getRetriableHttpClient(). + *

+ * This method allows setting a retry configuration that will be used for all + * subsequent calls + * to getRetriableHttpClient() that don't explicitly specify a retry + * configuration. + *

+ * + * @param retryConfig The retry configuration to use + */ + default void setRetryConfig(RetryConfig retryConfig) { + // Default implementation does nothing + } + } diff --git a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java new file mode 100644 index 000000000000..e6717bdf1975 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java @@ -0,0 +1,340 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.connections.httpclient; + +/** + * Configuration for HTTP client retry behavior. + *

+ * This class provides configuration options for HTTP client retry behavior when + * making requests. It allows customization of the maximum number of retry + * attempts, + * whether to retry on IO exceptions, exponential backoff settings, jitter for + * backoff times, + * and connection/socket timeouts. + *

+ * The default configuration is 0 retry attempts (no retries) with retries + * enabled for IO exceptions if configured, + * with exponential backoff starting at 1000ms and multiplying by 2.0 for each + * retry. + * Jitter is enabled by default with a factor of 0.5 to prevent synchronized + * retry storms. + *

+ * This configuration is used by the + * {@link HttpClientProvider#getRetriableHttpClient()} + * and {@link HttpClientProvider#getRetriableHttpClient(RetryConfig)} methods to + * create + * HTTP clients with retry capabilities. + *

+ * System properties that can be used to configure retry behavior: + *

    + *
  • {@code max-retries} - Maximum number of retry attempts (default: 0)
  • + *
  • {@code retry-io-exception} - Whether to retry on IO exceptions (default: + * true)
  • + *
  • {@code initial-backoff-millis} - Initial backoff time in milliseconds + * (default: 1000)
  • + *
  • {@code backoff-multiplier} - Multiplier for exponential backoff (default: + * 2.0)
  • + *
  • {@code jitter-factor} - Random jitter factor to apply to backoff times + * (default: 0.5)
  • + *
  • {@code use-jitter} - Whether to apply jitter to backoff times (default: + * true)
  • + *
  • {@code connection-timeout-millis} - Connection timeout in milliseconds + * (default: 10000)
  • + *
  • {@code socket-timeout-millis} - Socket timeout in milliseconds (default: + * 10000)
  • + *
+ *

+ * Example usage: + * + *

+ * RetryConfig config = new RetryConfig.Builder()
+ *         .maxRetries(5)
+ *         .retryOnIOException(true)
+ *         .initialBackoffMillis(1000)
+ *         .backoffMultiplier(2.0)
+ *         .useJitter(true)
+ *         .jitterFactor(0.5)
+ *         .connectionTimeoutMillis(10000)
+ *         .socketTimeoutMillis(10000)
+ *         .build();
+ *
+ * CloseableHttpClient client = httpClientProvider.getRetriableHttpClient(config);
+ * 
+ */ +public class RetryConfig { + private final int maxRetries; + private final boolean retryOnIOException; + private final long initialBackoffMillis; + private final double backoffMultiplier; + private final boolean useJitter; + private final double jitterFactor; + private final int connectionTimeoutMillis; + private final int socketTimeoutMillis; + + private RetryConfig(Builder builder) { + this.maxRetries = builder.maxRetries; + this.retryOnIOException = builder.retryOnIOException; + this.initialBackoffMillis = builder.initialBackoffMillis; + this.backoffMultiplier = builder.backoffMultiplier; + this.useJitter = builder.useJitter; + this.jitterFactor = builder.jitterFactor; + this.connectionTimeoutMillis = builder.connectionTimeoutMillis; + this.socketTimeoutMillis = builder.socketTimeoutMillis; + } + + /** + * Gets the maximum number of retry attempts. + * + * @return The maximum number of retry attempts + */ + public int getMaxRetries() { + return maxRetries; + } + + /** + * Determines whether to retry on IO exceptions. + * + * @return {@code true} if retries should be attempted on IO exceptions, + * {@code false} otherwise + */ + public boolean isRetryOnIOException() { + return retryOnIOException; + } + + /** + * Gets the initial backoff time in milliseconds before the first retry attempt. + * + * @return The initial backoff time in milliseconds + */ + public long getInitialBackoffMillis() { + return initialBackoffMillis; + } + + /** + * Gets the multiplier used for exponential backoff between retry attempts. + * + * @return The backoff multiplier + */ + public double getBackoffMultiplier() { + return backoffMultiplier; + } + + /** + * Gets the connection timeout in milliseconds. + * + * @return The connection timeout in milliseconds + */ + public int getConnectionTimeoutMillis() { + return connectionTimeoutMillis; + } + + /** + * Gets the socket timeout in milliseconds. + * + * @return The socket timeout in milliseconds + */ + public int getSocketTimeoutMillis() { + return socketTimeoutMillis; + } + + /** + * Determines whether to apply jitter to backoff times. + *

+ * Jitter adds randomness to backoff times to prevent synchronized retry storms + * when multiple clients are retrying at the same time. + * + * @return {@code true} if jitter should be applied, {@code false} otherwise + */ + public boolean isUseJitter() { + return useJitter; + } + + /** + * Gets the jitter factor to apply to backoff times. + *

+ * The jitter factor determines how much randomness to apply to the backoff + * time. + * A value of 0.5 means the actual backoff time will be between 50% and 150% of + * the calculated exponential backoff time. + * + * @return The jitter factor + */ + public double getJitterFactor() { + return jitterFactor; + } + + /** + * Builder for creating {@link RetryConfig} instances. + *

+ * This builder uses the following defaults: + *

    + *
  • maxRetries = 0
  • + *
  • retryOnIOException = true
  • + *
  • initialBackoffMillis = 1000
  • + *
  • backoffMultiplier = 2.0
  • + *
  • connectionTimeoutMillis = 10000
  • + *
  • socketTimeoutMillis = 10000
  • + *
+ */ + public static class Builder { + private int maxRetries = 0; + private boolean retryOnIOException = true; + private long initialBackoffMillis = 1000; + private double backoffMultiplier = 2.0; + private boolean useJitter = true; + private double jitterFactor = 0.5; + private int connectionTimeoutMillis = 10000; + private int socketTimeoutMillis = 10000; + + /** + * Sets the maximum number of retry attempts. + *

+ * The default value is 3. A value of 0 means no retries will be attempted. + * Negative values are allowed but not recommended as they don't make practical + * sense. + * + * @param maxRetries The maximum number of retry attempts + * @return This builder instance for method chaining + */ + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + /** + * Sets whether to retry on IO exceptions. + *

+ * The default value is {@code true}. When set to {@code false}, the client will + * not + * retry requests that fail with IO exceptions. + * + * @param retryOnIOException {@code true} to retry on IO exceptions, + * {@code false} otherwise + * @return This builder instance for method chaining + */ + public Builder retryOnIOException(boolean retryOnIOException) { + this.retryOnIOException = retryOnIOException; + return this; + } + + /** + * Sets the initial backoff time in milliseconds before the first retry attempt. + *

+ * The default value is 1000 (1 second). This is the amount of time to wait + * before + * the first retry attempt. Subsequent retry attempts will use exponential + * backoff + * based on this value and the backoff multiplier. + * + * @param initialBackoffMillis The initial backoff time in milliseconds + * @return This builder instance for method chaining + */ + public Builder initialBackoffMillis(long initialBackoffMillis) { + this.initialBackoffMillis = initialBackoffMillis; + return this; + } + + /** + * Sets the multiplier used for exponential backoff between retry attempts. + *

+ * The default value is 2.0. This means that each retry will wait twice as long + * as + * the previous retry. For example, with an initial backoff of 1000ms and a + * multiplier + * of 2.0, the retry delays would be: 1000ms, 2000ms, 4000ms, etc. + * + * @param backoffMultiplier The backoff multiplier + * @return This builder instance for method chaining + */ + public Builder backoffMultiplier(double backoffMultiplier) { + this.backoffMultiplier = backoffMultiplier; + return this; + } + + /** + * Sets the connection timeout in milliseconds. + *

+ * The default value is 10000 (10 seconds). This is the timeout for establishing + * a connection with the remote server. + * + * @param connectionTimeoutMillis The connection timeout in milliseconds + * @return This builder instance for method chaining + */ + public Builder connectionTimeoutMillis(int connectionTimeoutMillis) { + this.connectionTimeoutMillis = connectionTimeoutMillis; + return this; + } + + /** + * Sets the socket timeout in milliseconds. + *

+ * The default value is 10000 (10 seconds). This is the timeout for waiting for + * data + * from an established connection. + * + * @param socketTimeoutMillis The socket timeout in milliseconds + * @return This builder instance for method chaining + */ + public Builder socketTimeoutMillis(int socketTimeoutMillis) { + this.socketTimeoutMillis = socketTimeoutMillis; + return this; + } + + /** + * Sets whether to apply jitter to backoff times. + *

+ * The default value is {@code true}. When set to {@code true}, the system will + * add + * randomness to backoff times to prevent synchronized retry storms when + * multiple + * clients are retrying at the same time. + * + * @param useJitter {@code true} to apply jitter, {@code false} otherwise + * @return This builder instance for method chaining + */ + public Builder useJitter(boolean useJitter) { + this.useJitter = useJitter; + return this; + } + + /** + * Sets the jitter factor to apply to backoff times. + *

+ * The default value is 0.5. This means the actual backoff time will be between + * 50% and 150% of the calculated exponential backoff time. For example, if the + * calculated backoff time is 1000ms, the actual backoff time will be between + * 500ms and 1500ms. + * + * @param jitterFactor The jitter factor + * @return This builder instance for method chaining + */ + public Builder jitterFactor(double jitterFactor) { + this.jitterFactor = jitterFactor; + return this; + } + + /** + * Builds a new {@link RetryConfig} instance with the current builder settings. + * + * @return A new {@link RetryConfig} instance + */ + public RetryConfig build() { + return new RetryConfig(this); + } + } +} \ No newline at end of file diff --git a/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java b/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java index 09a0a7588c64..cc82b48bb455 100644 --- a/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java @@ -30,7 +30,6 @@ import java.util.List; import org.apache.http.HttpHeaders; -import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ByteArrayEntity; @@ -41,7 +40,6 @@ import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.models.KeycloakSession; - /** * @author Peter Nalyvayko * @version $Revision: 1 $ @@ -52,7 +50,7 @@ public abstract class OCSPProvider { private final static Logger logger = Logger.getLogger(OCSPProvider.class); - protected static int OCSP_CONNECT_TIMEOUT = 10000; // 10 sec + protected static final int OCSP_CONNECT_TIMEOUT = 10000; // 10 sec protected static final int TIME_SKEW = 900000; public enum RevocationStatus { @@ -87,6 +85,7 @@ public OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, return check(session, cert, issuerCertificate, Collections.singletonList(responderURI), responderCert, date); } + /** * Requests certificate revocation status using OCSP. The OCSP responder URI * is obtained from the certificate's AIA extension. @@ -123,16 +122,10 @@ public OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, protected byte[] getEncodedOCSPResponse(KeycloakSession session, byte[] encodedOCSPReq, URI responderUri) throws IOException { - CloseableHttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient(); + // Use the retriable HTTP client which will apply retry and timeout settings from RetryConfig + CloseableHttpClient httpClient = session.getProvider(HttpClientProvider.class).getRetriableHttpClient(); HttpPost post = new HttpPost(responderUri); post.setHeader(HttpHeaders.CONTENT_TYPE, "application/ocsp-request"); - - final RequestConfig params = RequestConfig.custom() - .setConnectTimeout(OCSP_CONNECT_TIMEOUT) - .setSocketTimeout(OCSP_CONNECT_TIMEOUT) - .build(); - post.setConfig(params); - post.setEntity(new ByteArrayEntity(encodedOCSPReq)); //Get Response @@ -180,7 +173,6 @@ protected abstract OCSPRevocationStatus check(KeycloakSession session, X509Certi X509Certificate issuerCertificate, List responderURIs, X509Certificate responderCert, Date date) throws CertPathValidatorException; - protected static OCSPRevocationStatus unknownStatus() { return new OCSPRevocationStatus() { @Override @@ -209,5 +201,4 @@ public CRLReason getRevocationReason() { */ protected abstract List getResponderURIs(X509Certificate cert) throws CertificateEncodingException; - } diff --git a/server-spi-private/src/test/java/org/keycloak/connections/httpclient/RetryConfigTest.java b/server-spi-private/src/test/java/org/keycloak/connections/httpclient/RetryConfigTest.java new file mode 100644 index 000000000000..89016877da9e --- /dev/null +++ b/server-spi-private/src/test/java/org/keycloak/connections/httpclient/RetryConfigTest.java @@ -0,0 +1,173 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.connections.httpclient; + +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Comprehensive tests for RetryConfig class. + */ +public class RetryConfigTest { + + @Test + public void testDefaultValues() { + RetryConfig config = new RetryConfig.Builder().build(); + assertEquals(0, config.getMaxRetries()); + assertTrue(config.isRetryOnIOException()); + assertEquals(1000, config.getInitialBackoffMillis()); + assertEquals(2.0, config.getBackoffMultiplier(), 0.001); + assertEquals(10000, config.getConnectionTimeoutMillis()); + assertEquals(10000, config.getSocketTimeoutMillis()); + } + + @Test + public void testCustomValues() { + RetryConfig config = new RetryConfig.Builder() + .maxRetries(5) + .retryOnIOException(false) + .initialBackoffMillis(2000) + .backoffMultiplier(3.0) + .connectionTimeoutMillis(15000) + .socketTimeoutMillis(20000) + .build(); + + assertEquals(5, config.getMaxRetries()); + assertFalse(config.isRetryOnIOException()); + assertEquals(2000, config.getInitialBackoffMillis()); + assertEquals(3.0, config.getBackoffMultiplier(), 0.001); + assertEquals(15000, config.getConnectionTimeoutMillis()); + assertEquals(20000, config.getSocketTimeoutMillis()); + } + + @Test + public void testZeroRetries() { + RetryConfig config = new RetryConfig.Builder() + .maxRetries(0) + .build(); + + assertEquals(0, config.getMaxRetries()); + assertTrue(config.isRetryOnIOException()); + } + + @Test + public void testNegativeRetries() { + // Negative values should be allowed (though they don't make practical sense) + // This tests that the builder doesn't enforce any validation + RetryConfig config = new RetryConfig.Builder() + .maxRetries(-1) + .build(); + + assertEquals(-1, config.getMaxRetries()); + } + + @Test + public void testLargeNumberOfRetries() { + // Test with a large number of retries + RetryConfig config = new RetryConfig.Builder() + .maxRetries(Integer.MAX_VALUE) + .build(); + + assertEquals(Integer.MAX_VALUE, config.getMaxRetries()); + } + + @Test + public void testBuilderChaining() { + // Test that builder methods can be chained + RetryConfig config = new RetryConfig.Builder() + .maxRetries(10) + .retryOnIOException(false) + .build(); + + assertEquals(10, config.getMaxRetries()); + assertFalse(config.isRetryOnIOException()); + } + + @Test + public void testBuilderOverriding() { + // Test that later builder calls override earlier ones + RetryConfig config = new RetryConfig.Builder() + .maxRetries(5) + .maxRetries(10) + .retryOnIOException(true) + .retryOnIOException(false) + .initialBackoffMillis(500) + .initialBackoffMillis(1500) + .backoffMultiplier(1.5) + .backoffMultiplier(2.5) + .connectionTimeoutMillis(5000) + .connectionTimeoutMillis(8000) + .socketTimeoutMillis(12000) + .socketTimeoutMillis(15000) + .build(); + + assertEquals(10, config.getMaxRetries()); + assertFalse(config.isRetryOnIOException()); + assertEquals(1500, config.getInitialBackoffMillis()); + assertEquals(2.5, config.getBackoffMultiplier(), 0.001); + assertEquals(8000, config.getConnectionTimeoutMillis()); + assertEquals(15000, config.getSocketTimeoutMillis()); + } + + @Test + public void testExponentialBackoffSettings() { + // Test specific exponential backoff settings + RetryConfig config = new RetryConfig.Builder() + .initialBackoffMillis(100) // Very short initial backoff + .backoffMultiplier(4.0) // Aggressive multiplier + .build(); + + assertEquals(100, config.getInitialBackoffMillis()); + assertEquals(4.0, config.getBackoffMultiplier(), 0.001); + } + + @Test + public void testTimeoutSettings() { + // Test specific timeout settings + RetryConfig config = new RetryConfig.Builder() + .connectionTimeoutMillis(30000) // 30 seconds connection timeout + .socketTimeoutMillis(60000) // 60 seconds socket timeout + .build(); + + assertEquals(30000, config.getConnectionTimeoutMillis()); + assertEquals(60000, config.getSocketTimeoutMillis()); + } + + @Test + public void testJitterSettings() { + // Test jitter settings + RetryConfig config = new RetryConfig.Builder() + .useJitter(true) + .jitterFactor(0.3) + .build(); + + assertTrue(config.isUseJitter()); + assertEquals(0.3, config.getJitterFactor(), 0.001); + } + + @Test + public void testDisableJitter() { + // Test disabling jitter + RetryConfig config = new RetryConfig.Builder() + .useJitter(false) + .build(); + + assertFalse(config.isUseJitter()); + assertEquals(0.5, config.getJitterFactor(), 0.001); // Default value should still be set + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java index 7f4886f67ffb..7d63ba5fef01 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java @@ -46,7 +46,6 @@ import org.keycloak.models.UserModel; import org.keycloak.services.x509.X509ClientCertificateLookup; - /** * @author Peter Nalyvayko * @version $Revision: 1 $ @@ -61,6 +60,8 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth public static final String ENABLE_CRL = "x509-cert-auth.crl-checking-enabled"; public static final String ENABLE_OCSP = "x509-cert-auth.ocsp-checking-enabled"; public static final String OCSP_FAIL_OPEN = "x509-cert-auth.ocsp-fail-open"; + public static final String OCSP_MAX_RETRIES = "x509-cert-auth.ocsp-max-retries"; + public static final String OCSP_TIMEOUT_MILLIS = "x509-cert-auth.ocsp-timeout-millis"; public static final String ENABLE_CRLDP = "x509-cert-auth.crldp-checking-enabled"; public static final String CANONICAL_DN = "x509-cert-auth.canonical-dn-enabled"; public static final String TIMESTAMP_VALIDATION = "x509-cert-auth.timestamp-validation-enabled"; @@ -106,6 +107,12 @@ protected static class CertificateValidatorConfigBuilder { static CertificateValidator.CertificateValidatorBuilder fromConfig(KeycloakSession session, X509AuthenticatorConfigModel config) throws Exception { CertificateValidator.CertificateValidatorBuilder builder = new CertificateValidator.CertificateValidatorBuilder(); + + // Store the config in the session so it can be accessed by the + // CertificateValidator + // This allows passing the OCSP retry settings to the validator + session.setAttribute("x509-auth-config", config); + return builder .session(session) .keyUsage() @@ -271,7 +278,6 @@ protected X509Certificate[] getCertificateChain(AuthenticationFlowContext contex return null; } - // Saving some notes for audit to authSession as the event may not be necessarily triggered in this HTTP request where the certificate was parsed // For example if there is confirmation page enabled, it will be in the additional request protected void saveX509CertificateAuditDataToAuthSession(AuthenticationFlowContext context, @@ -292,15 +298,16 @@ private void recordX509DetailFromAuthSessionToEvent(AuthenticationFlowContext co context.getEvent().detail(detailName, detailValue); } - // Purely for unit testing public UserIdentityExtractor getUserIdentityExtractor(X509AuthenticatorConfigModel config) { return UserIdentityExtractorBuilder.fromConfig(config); } + // Purely for unit testing public UserIdentityToModelMapper getUserIdentityToModelMapper(X509AuthenticatorConfigModel config) { return UserIdentityToModelMapperBuilder.fromConfig(config); } + @Override public boolean requiresUser() { return false; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java index 9828f9c609d9..2efefb7ec658 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java @@ -115,7 +115,6 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen serialnumberHex.setDefaultValue(Boolean.toString(false)); serialnumberHex.setHelpText("Use the hex representation of the serial number. This option is relevant for authenticators using serial number."); - ProviderConfigProperty regExp = new ProviderConfigProperty(); regExp.setType(STRING_TYPE); regExp.setName(REGULAR_EXPRESSION); @@ -196,6 +195,20 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen ocspFailOpen.setHelpText("Whether to allow or deny authentication for client certificates that have missing/invalid/inconclusive OCSP endpoints. By default a successful OCSP response is required."); ocspFailOpen.setLabel("OCSP Fail-Open Behavior"); + ProviderConfigProperty ocspMaxRetries = new ProviderConfigProperty(); + ocspMaxRetries.setType(ProviderConfigProperty.STRING_TYPE); + ocspMaxRetries.setName(OCSP_MAX_RETRIES); + ocspMaxRetries.setDefaultValue("0"); + ocspMaxRetries.setHelpText("Maximum number of retry attempts for OCSP requests that fail due to network issues."); + ocspMaxRetries.setLabel("OCSP Max Retries"); + + ProviderConfigProperty ocspTimeoutMillis = new ProviderConfigProperty(); + ocspTimeoutMillis.setType(ProviderConfigProperty.STRING_TYPE); + ocspTimeoutMillis.setName(OCSP_TIMEOUT_MILLIS); + ocspTimeoutMillis.setDefaultValue("10000"); + ocspTimeoutMillis.setHelpText("Timeout in milliseconds for OCSP requests. This applies to both connection and socket timeouts."); + ocspTimeoutMillis.setLabel("OCSP Timeout (ms)"); + ProviderConfigProperty ocspResponderUri = new ProviderConfigProperty(); ocspResponderUri.setType(STRING_TYPE); ocspResponderUri.setName(OCSPRESPONDER_URI); @@ -261,6 +274,8 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen cRLAbortIfNonUpdated, oCspCheckingEnabled, ocspFailOpen, + ocspMaxRetries, + ocspTimeoutMillis, ocspResponderUri, ocspResponderCert, keyUsage, diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java index 8275255b8c71..3df83cab6a48 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java @@ -179,11 +179,26 @@ public static class BouncyCastleOCSPChecker extends OCSPChecker { private final KeycloakSession session; private final String responderUri; private final X509Certificate responderCert; + private final int maxRetries; + private final int timeoutMillis; BouncyCastleOCSPChecker(KeycloakSession session, String responderUri, X509Certificate responderCert) { this.session = session; this.responderUri = responderUri; this.responderCert = responderCert; + + // Default values + this.maxRetries = 0; // No retries by default + this.timeoutMillis = 10000; + } + + BouncyCastleOCSPChecker(KeycloakSession session, String responderUri, X509Certificate responderCert, + int maxRetries, int timeoutMillis) { + this.session = session; + this.responderUri = responderUri; + this.responderCert = responderCert; + this.maxRetries = maxRetries; + this.timeoutMillis = timeoutMillis; } @Override @@ -201,6 +216,22 @@ public OCSPProvider.OCSPRevocationStatus check(X509Certificate cert, X509Certifi // 2) Includes the value of OCSPsigning in ExtendedKeyUsage v3 extension // 3) Certificate is valid at the time OCSPProvider ocspProvider = CryptoIntegration.getProvider().getOCSPProver(OCSPProvider.class); + + // Configure retry behavior + HttpClientProvider httpProvider = session.getProvider(HttpClientProvider.class); + if (httpProvider != null && maxRetries > 0) { + // Use the retriable HTTP client with configured max retries + org.keycloak.connections.httpclient.RetryConfig retryConfig = new org.keycloak.connections.httpclient.RetryConfig.Builder() + .maxRetries(maxRetries) + .connectionTimeoutMillis(timeoutMillis) + .socketTimeoutMillis(timeoutMillis) + .build(); + + httpProvider.setRetryConfig(retryConfig); + logger.debugf("OCSP check configured with maxRetries=%d, timeoutMillis=%d", maxRetries, + timeoutMillis); + } + ocspRevocationStatus = ocspProvider.check(session, cert, issuerCertificate); } else { @@ -213,6 +244,22 @@ public OCSPProvider.OCSPRevocationStatus check(X509Certificate cert, X509Certifi } logger.tracef("Responder URI \"%s\" will be used to verify revocation status of the certificate using OCSP with responderCert=%s", uri.toString(), responderCert); + + // Configure retry behavior + HttpClientProvider httpProvider = session.getProvider(HttpClientProvider.class); + if (httpProvider != null && maxRetries > 0) { + // Use the retriable HTTP client with configured max retries + org.keycloak.connections.httpclient.RetryConfig retryConfig = new org.keycloak.connections.httpclient.RetryConfig.Builder() + .maxRetries(maxRetries) + .connectionTimeoutMillis(timeoutMillis) + .socketTimeoutMillis(timeoutMillis) + .build(); + + httpProvider.setRetryConfig(retryConfig); + logger.debugf("OCSP check configured with maxRetries=%d, timeoutMillis=%d", maxRetries, + timeoutMillis); + } + // Obtains the revocation status of a certificate using OCSP. // OCSP responder's certificate is assumed to be the issuer's certificate // certificate. @@ -1095,12 +1142,25 @@ public CertificateValidator build(X509Certificate[] certs) { if (_crlLoader == null) { _crlLoader = new CRLFileLoader(session, "", _crlAbortIfNonUpdated); } + + // Get OCSP retry settings from X509AuthenticatorConfigModel if available + int ocspMaxRetries = 0; // Default value (no retries) + int ocspTimeoutMillis = 10000; // Default value + + if (session != null) { + X509AuthenticatorConfigModel config = (X509AuthenticatorConfigModel) session + .getAttribute("x509-auth-config"); + if (config != null) { + ocspMaxRetries = config.getOCSPMaxRetries(); + ocspTimeoutMillis = config.getOCSPTimeoutMillis(); + } + } + return new CertificateValidator(certs, _keyUsageBits, _extendedKeyUsage, _certificatePolicy, _certificatePolicyMode, _crlCheckingEnabled, _crlAbortIfNonUpdated, _crldpEnabled, _crlLoader, _ocspEnabled, _ocspFailOpen, - new BouncyCastleOCSPChecker(session, _responderUri, _responderCert), session, _timestampValidationEnabled, _trustValidationEnabled); + new BouncyCastleOCSPChecker(session, _responderUri, _responderCert, ocspMaxRetries, ocspTimeoutMillis), session, _timestampValidationEnabled, _trustValidationEnabled); } } - } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java index 7fe0106a9999..ab676e0c0696 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java @@ -139,6 +139,26 @@ public X509AuthenticatorConfigModel setOCSPFailOpen(boolean value) { return this; } + public int getOCSPMaxRetries() { + String value = getConfig().get(OCSP_MAX_RETRIES); + return value != null ? Integer.parseInt(value) : 0; // Default to 0 retries (no retry) + } + + public X509AuthenticatorConfigModel setOCSPMaxRetries(int value) { + getConfig().put(OCSP_MAX_RETRIES, Integer.toString(value)); + return this; + } + + public int getOCSPTimeoutMillis() { + String value = getConfig().get(OCSP_TIMEOUT_MILLIS); + return value != null ? Integer.parseInt(value) : 10000; // Default to 10 seconds + } + + public X509AuthenticatorConfigModel setOCSPTimeoutMillis(int value) { + getConfig().put(OCSP_TIMEOUT_MILLIS, Integer.toString(value)); + return this; + } + public boolean getCRLDistributionPointEnabled() { return Boolean.parseBoolean(getConfig().get(ENABLE_CRLDP)); } diff --git a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java index 6c87b731bac7..b241611aef86 100755 --- a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java +++ b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java @@ -75,6 +75,9 @@ public class DefaultHttpClientFactory implements HttpClientFactory { private final InputStreamResponseHandler inputStreamResponseHandler = new InputStreamResponseHandler(); private long maxConsumedResponseSize; + // Retry configuration + private RetryConfig defaultRetryConfig; + private static class InputStreamResponseHandler extends AbstractResponseHandler { public InputStream handleEntity(HttpEntity entity) throws IOException { @@ -96,6 +99,116 @@ public CloseableHttpClient getHttpClient() { return httpClient; } + @Override + public CloseableHttpClient getRetriableHttpClient() { + return getRetriableHttpClient(defaultRetryConfig); + } + + @Override + public CloseableHttpClient getRetriableHttpClient(RetryConfig retryConfig) { + // If retries are disabled, just return the default client + if (retryConfig == null || retryConfig.getMaxRetries() <= 0) { + return getHttpClient(); + } + + // Create HTTP client builder + HttpClientBuilder builder = newHttpClientBuilder(); + + // Configure basic settings + long socketTimeout = retryConfig.getSocketTimeoutMillis(); + long establishConnectionTimeout = retryConfig.getConnectionTimeoutMillis(); + int maxPooledPerRoute = config.getInt("max-pooled-per-route", 64); + int connectionPoolSize = config.getInt("connection-pool-size", 128); + long connectionTTL = config.getLong("connection-ttl-millis", -1L); + long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L); + boolean disableCookies = config.getBoolean("disable-cookies", true); + boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false); + boolean reuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true); + + // Configure builder + builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) + .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) + .maxPooledPerRoute(maxPooledPerRoute) + .connectionPoolSize(connectionPoolSize) + .connectionTTL(connectionTTL, TimeUnit.MILLISECONDS) + .maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS) + .disableCookies(disableCookies) + .proxyMappings(DefaultHttpClientFactory.this.configureProxySettings()) + .expectContinueEnabled(expectContinueEnabled) + .reuseConnections(reuseConnections); + + // Configure security settings + DefaultHttpClientFactory.this.configureSecuritySettings(session, builder); + + // Configure retry handler + if (retryConfig.getMaxRetries() > 0) { + builder.getApacheHttpClientBuilder().setRetryHandler( + new org.apache.http.impl.client.DefaultHttpRequestRetryHandler( + retryConfig.getMaxRetries(), retryConfig.isRetryOnIOException()) { + @Override + public boolean retryRequest(IOException exception, int executionCount, + org.apache.http.protocol.HttpContext context) { + boolean shouldRetry = super.retryRequest(exception, executionCount, context); + + if (shouldRetry) { + // Calculate exponential backoff delay with jitter if enabled + long baseDelay = (long) (retryConfig.getInitialBackoffMillis() * + Math.pow(retryConfig.getBackoffMultiplier(), executionCount - 1)); + long delayMillis = calculateBackoffDelay(executionCount, retryConfig); + + // Log retry attempt + if (retryConfig.isUseJitter()) { + logger.debugf( + "Retrying HTTP request (attempt %d) with backoff delay %d ms (base: %d ms, jitter factor: %.2f): %s", + executionCount, delayMillis, baseDelay, + retryConfig.getJitterFactor(), exception.getMessage()); + } else { + logger.debugf( + "Retrying HTTP request (attempt %d) with backoff delay %d ms: %s", + executionCount, delayMillis, exception.getMessage()); + } + + try { + // Sleep for the calculated delay + Thread.sleep(delayMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; // Don't retry if interrupted + } + } else if (executionCount > retryConfig.getMaxRetries()) { + logger.debugf("Not retrying HTTP request after %d attempts: %s", + executionCount - 1, exception.getMessage()); + } + return shouldRetry; + } + }); + } + + return builder.build(); + } + + /** + * Calculates the backoff delay with optional jitter + */ + private long calculateBackoffDelay(int executionCount, RetryConfig config) { + // executionCount starts at 1, so we need to subtract 1 to get the retry number + int retryNumber = executionCount - 1; + + // Calculate base exponential backoff: initialBackoff * (multiplier ^ + // retryNumber) + long baseDelay = (long) (config.getInitialBackoffMillis() + * Math.pow(config.getBackoffMultiplier(), retryNumber)); + + // Apply jitter if enabled + if (config.isUseJitter()) { + double jitterFactor = config.getJitterFactor(); + double randomFactor = 1.0 - jitterFactor + (Math.random() * jitterFactor * 2.0); + return (long) (baseDelay * randomFactor); + } else { + return baseDelay; + } + } + @Override public void close() { @@ -165,90 +278,150 @@ public String getId() { @Override public void init(Config.Scope config) { this.config = config; + + // Initialize default retry configuration + int maxRetries = config.getInt("http-client.default-max-retries", 0); // No retries by default + boolean retryOnIOException = config.getBoolean("http-client.default-retry-on-io-exception", true); + long initialBackoffMillis = config.getLong("http-client.default-initial-backoff-millis", 1000L); + + // Get backoff multiplier as a string and convert to double (Config.Scope + // doesn't have getDouble) + String backoffMultiplierStr = config.get("http-client.default-backoff-multiplier", "2.0"); + double backoffMultiplier = Double.parseDouble(backoffMultiplierStr); + + // Get jitter settings + boolean useJitter = config.getBoolean("http-client.default-use-jitter", true); + String jitterFactorStr = config.get("http-client.default-jitter-factor", "0.5"); + double jitterFactor = Double.parseDouble(jitterFactorStr); + + int connectionTimeoutMillis = config.getInt("http-client.default-connection-timeout-millis", 10000); + int socketTimeoutMillis = config.getInt("http-client.default-socket-timeout-millis", 10000); + + defaultRetryConfig = new RetryConfig.Builder() + .maxRetries(maxRetries) + .retryOnIOException(retryOnIOException) + .initialBackoffMillis(initialBackoffMillis) + .backoffMultiplier(backoffMultiplier) + .useJitter(useJitter) + .jitterFactor(jitterFactor) + .connectionTimeoutMillis(connectionTimeoutMillis) + .socketTimeoutMillis(socketTimeoutMillis) + .build(); } private void lazyInit(KeycloakSession session) { if (httpClient == null) { synchronized(this) { if (httpClient == null) { - long socketTimeout = config.getLong("socket-timeout-millis", 5000L); - long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L); - int maxPooledPerRoute = config.getInt("max-pooled-per-route", 64); - int connectionPoolSize = config.getInt("connection-pool-size", 128); - long connectionTTL = config.getLong("connection-ttl-millis", -1L); - long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L); - boolean disableCookies = config.getBoolean("disable-cookies", true); - String clientKeystore = config.get("client-keystore"); - String clientKeystorePassword = config.get("client-keystore-password"); - String clientPrivateKeyPassword = config.get("client-key-password"); - boolean disableTrustManager = config.getBoolean("disable-trust-manager", false); - - boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false); - boolean reuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true); - - // optionally configure proxy mappings - // direct SPI config (e.g. via standalone.xml) takes precedence over env vars - // lower case env vars take precedence over upper case env vars - ProxyMappings proxyMappings = ProxyMappings.valueOf(config.getArray("proxy-mappings")); - if (proxyMappings == null || proxyMappings.isEmpty()) { - logger.debug("Trying to use proxy mapping from env vars"); - String httpProxy = getEnvVarValue(HTTPS_PROXY); - if (isBlank(httpProxy)) { - httpProxy = getEnvVarValue(HTTP_PROXY); - } - String noProxy = getEnvVarValue(NO_PROXY); - - logger.debugf("httpProxy: %s, noProxy: %s", httpProxy, noProxy); - proxyMappings = ProxyMappings.withFixedProxyMapping(httpProxy, noProxy); - } + // Create the default HTTP client with no retries + httpClient = createHttpClientWithoutRetries(session); + } + } + } + } - HttpClientBuilder builder = newHttpClientBuilder(); - - builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) - .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) - .maxPooledPerRoute(maxPooledPerRoute) - .connectionPoolSize(connectionPoolSize) - .connectionTTL(connectionTTL, TimeUnit.MILLISECONDS) - .maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS) - .disableCookies(disableCookies) - .proxyMappings(proxyMappings) - .expectContinueEnabled(expectContinueEnabled) - .reuseConnections(reuseConnections); - - TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class); - boolean disableTruststoreProvider = truststoreProvider == null || truststoreProvider.getTruststore() == null; - - if (disableTruststoreProvider) { - logger.warn("TruststoreProvider is disabled"); - } else { - builder.hostnameVerification(truststoreProvider.getPolicy()); - try { - builder.trustStore(truststoreProvider.getTruststore()); - } catch (Exception e) { - throw new RuntimeException("Failed to load truststore", e); - } - } + /** + * Creates an HTTP client without retry functionality + */ + private CloseableHttpClient createHttpClientWithoutRetries(KeycloakSession session) { + // Create HTTP client builder + HttpClientBuilder builder = newHttpClientBuilder(); + + // Configure basic settings + long socketTimeout = config.getLong("socket-timeout-millis", 5000L); + long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L); + int maxPooledPerRoute = config.getInt("max-pooled-per-route", 64); + int connectionPoolSize = config.getInt("connection-pool-size", 128); + long connectionTTL = config.getLong("connection-ttl-millis", -1L); + long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L); + boolean disableCookies = config.getBoolean("disable-cookies", true); + boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false); + boolean reuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true); + + // Configure builder + builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) + .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) + .maxPooledPerRoute(maxPooledPerRoute) + .connectionPoolSize(connectionPoolSize) + .connectionTTL(connectionTTL, TimeUnit.MILLISECONDS) + .maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS) + .disableCookies(disableCookies) + .proxyMappings(configureProxySettings()) + .expectContinueEnabled(expectContinueEnabled) + .reuseConnections(reuseConnections); + + // Configure security settings + configureSecuritySettings(session, builder); + + // Build the client + return builder.build(); + } - if (disableTrustManager) { - logger.warn("TrustManager is disabled"); - builder.disableTrustManager(); - } + /** + * Configures security settings for an HTTP client builder. + * + * @param session The Keycloak session + * @param builder The HTTP client builder to configure + */ + private void configureSecuritySettings(KeycloakSession session, HttpClientBuilder builder) { + // Configure TrustStore + TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class); + boolean disableTruststoreProvider = truststoreProvider == null || truststoreProvider.getTruststore() == null; + + if (disableTruststoreProvider) { + logger.warn("TruststoreProvider is disabled"); + } else { + builder.hostnameVerification(truststoreProvider.getPolicy()); + try { + builder.trustStore(truststoreProvider.getTruststore()); + } catch (Exception e) { + throw new RuntimeException("Failed to load truststore", e); + } + } - if (clientKeystore != null) { - clientKeystore = EnvUtil.replace(clientKeystore); - try { - KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword); - builder.keyStore(clientCertKeystore, clientPrivateKeyPassword); - } catch (Exception e) { - throw new RuntimeException("Failed to load keystore", e); - } - } - httpClient = builder.build(); - } + // Configure TrustManager + boolean disableTrustManager = config.getBoolean("disable-trust-manager", false); + if (disableTrustManager) { + logger.warn("TrustManager is disabled"); + builder.disableTrustManager(); + } + + // Configure KeyStore + String clientKeystore = config.get("client-keystore"); + if (clientKeystore != null) { + clientKeystore = EnvUtil.replace(clientKeystore); + String clientKeystorePassword = config.get("client-keystore-password"); + String clientPrivateKeyPassword = config.get("client-key-password"); + try { + KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword); + builder.keyStore(clientCertKeystore, clientPrivateKeyPassword); + } catch (Exception e) { + throw new RuntimeException("Failed to load keystore", e); } } } + /** + * Configures proxy settings for HTTP clients. + * + * @return ProxyMappings configured based on settings or environment variables + */ + private ProxyMappings configureProxySettings() { + ProxyMappings proxyMappings = ProxyMappings.valueOf(config.getArray("proxy-mappings")); + if (proxyMappings == null || proxyMappings.isEmpty()) { + logger.debug("Trying to use proxy mapping from env vars"); + String httpProxy = getEnvVarValue(HTTPS_PROXY); + if (isBlank(httpProxy)) { + httpProxy = getEnvVarValue(HTTP_PROXY); + } + String noProxy = getEnvVarValue(NO_PROXY); + + logger.debugf("httpProxy: %s, noProxy: %s", httpProxy, noProxy); + proxyMappings = ProxyMappings.withFixedProxyMapping(httpProxy, noProxy); + } + return proxyMappings; + } + protected HttpClientBuilder newHttpClientBuilder() { return new HttpClientBuilder(); } @@ -342,6 +515,57 @@ public List getConfigMetadata() { .helpText("Maximum size of a response consumed by the client (to prevent denial of service)") .defaultValue(HttpClientProvider.DEFAULT_MAX_CONSUMED_RESPONSE_SIZE) .add() + .property() + .name("http-client.default-max-retries") + .type("int") + .helpText("Maximum number of retry attempts for HTTP requests.") + .defaultValue(3) + .add() + .property() + .name("http-client.default-retry-on-io-exception") + .type("boolean") + .helpText("Whether to retry HTTP requests on IO exceptions.") + .defaultValue(true) + .add() + .property() + .name("http-client.default-initial-backoff-millis") + .type("long") + .helpText("Initial backoff time in milliseconds before the first retry attempt.") + .defaultValue(1000L) + .add() + .property() + .name("http-client.default-backoff-multiplier") + .type("string") + .helpText( + "Multiplier for exponential backoff between retry attempts. For example, with an initial backoff of 1000ms and a multiplier of 2.0, the retry delays would be: 1000ms, 2000ms, 4000ms, etc.") + .defaultValue("2.0") + .add() + .property() + .name("http-client.default-use-jitter") + .type("boolean") + .helpText( + "Whether to apply jitter to backoff times to prevent synchronized retry storms when multiple clients are retrying at the same time.") + .defaultValue(true) + .add() + .property() + .name("http-client.default-jitter-factor") + .type("string") + .helpText( + "Jitter factor to apply to backoff times. A value of 0.5 means the actual backoff time will be between 50% and 150% of the calculated exponential backoff time.") + .defaultValue("0.5") + .add() + .property() + .name("http-client.default-connection-timeout-millis") + .type("int") + .helpText("Connection timeout in milliseconds for retriable HTTP clients.") + .defaultValue(10000) + .add() + .property() + .name("http-client.default-socket-timeout-millis") + .type("int") + .helpText("Socket timeout in milliseconds for retriable HTTP clients.") + .defaultValue(10000) + .add() .build(); } diff --git a/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java b/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java index 168de62e3016..8f4934b6722f 100644 --- a/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java +++ b/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java @@ -42,29 +42,53 @@ public class DefaultHttpClientFactoryTest { private static final String DISABLE_TRUST_MANAGER_PROPERTY = "disable-trust-manager"; private static final String TEST_DOMAIN = "keycloak.org"; + private static final String MAX_RETRIES_PROPERTY = "max-retries"; + private static final String RETRY_IO_EXCEPTION_PROPERTY = "retry-io-exception"; + + // Common objects for tests + private DefaultHttpClientFactory factory; + private KeycloakSession session; + + /** + * Helper method to create and initialize factory with default settings + */ + private HttpClientProvider createDefaultProvider() { + factory = new DefaultHttpClientFactory(); + factory.init(ScopeUtil.createScope(new HashMap<>())); + session = new ResteasyKeycloakSession(new ResteasyKeycloakSessionFactory()); + return factory.create(session); + } + + /** + * Helper method to create and initialize factory with custom settings + */ + private HttpClientProvider createProviderWithProperties(Map values) { + factory = new DefaultHttpClientFactory(); + factory.init(ScopeUtil.createScope(values)); + session = new ResteasyKeycloakSession(new ResteasyKeycloakSessionFactory()); + return factory.create(session); + } @Test - public void createHttpClientProviderWithDisableTrustManager() throws IOException{ + public void createHttpClientProviderWithDisableTrustManager() throws IOException { + // Create provider with trust manager disabled Map values = new HashMap<>(); values.put(DISABLE_TRUST_MANAGER_PROPERTY, "true"); - DefaultHttpClientFactory factory = new DefaultHttpClientFactory(); - factory.init(ScopeUtil.createScope(values)); - KeycloakSession session = new ResteasyKeycloakSession(new ResteasyKeycloakSessionFactory()); - HttpClientProvider provider = factory.create(session); - Optional testURL = getTestURL(); - Assume.assumeTrue( "Could not get test url for domain", testURL.isPresent() ); + HttpClientProvider provider = createProviderWithProperties(values); + + Optional testURL = getTestURL(); + Assume.assumeTrue("Could not get test url for domain", testURL.isPresent()); try (CloseableHttpClient httpClient = provider.getHttpClient(); - CloseableHttpResponse response = httpClient.execute(new HttpGet(testURL.get()))) { - assertEquals(HttpStatus.SC_NOT_FOUND,response.getStatusLine().getStatusCode()); + CloseableHttpResponse response = httpClient.execute(new HttpGet(testURL.get()))) { + assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusLine().getStatusCode()); } } @Test(expected = SSLPeerUnverifiedException.class) public void createHttpClientProviderWithUnvailableURL() throws IOException { - DefaultHttpClientFactory factory = new DefaultHttpClientFactory(); - factory.init(ScopeUtil.createScope(new HashMap<>())); - KeycloakSession session = new ResteasyKeycloakSession(new ResteasyKeycloakSessionFactory()); - HttpClientProvider provider = factory.create(session); + // Create provider with default settings + HttpClientProvider provider = createDefaultProvider(); + try (CloseableHttpClient httpClient = provider.getHttpClient()) { Optional testURL = getTestURL(); Assume.assumeTrue("Could not get test url for domain", testURL.isPresent()); @@ -72,6 +96,53 @@ public void createHttpClientProviderWithUnvailableURL() throws IOException { } } + @Test + public void testGetRetriableHttpClient() throws IOException { + // Create provider with default retry config + HttpClientProvider provider = createDefaultProvider(); + + // Get retriable HTTP client with default config + CloseableHttpClient client = provider.getRetriableHttpClient(); + + // Verify client is not null + org.junit.Assert.assertNotNull("Retriable HTTP client should not be null", client); + } + + @Test + public void testGetRetriableHttpClientWithCustomConfig() throws IOException { + // Create provider with default settings + HttpClientProvider provider = createDefaultProvider(); + + // Create custom retry config + RetryConfig config = new RetryConfig.Builder() + .maxRetries(5) + .retryOnIOException(false) + .build(); + + // Get retriable HTTP client with custom config + CloseableHttpClient client = provider.getRetriableHttpClient(config); + + // Verify client is not null + org.junit.Assert.assertNotNull("Retriable HTTP client with custom config should not be null", client); + } + + @Test + public void testFactoryInitWithRetryProperties() { + // Create factory with custom retry properties + Map values = new HashMap<>(); + values.put(MAX_RETRIES_PROPERTY, "5"); + values.put(RETRY_IO_EXCEPTION_PROPERTY, "false"); + + // Create provider with custom properties + HttpClientProvider provider = createProviderWithProperties(values); + + // Get retriable HTTP client + CloseableHttpClient client = provider.getRetriableHttpClient(); + + // Verify client is not null + org.junit.Assert.assertNotNull("Retriable HTTP client should not be null", client); + } + private Optional getTestURL() { try { // Convert domain name to ip to make request by ip diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java index 6ff7674b28d1..53d99cd76fa8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java @@ -23,6 +23,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator; import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel; import org.keycloak.common.util.PemUtils; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; @@ -64,6 +65,10 @@ public class X509OCSPResponderTest extends AbstractX509AuthenticationTest { private static final int OCSP_RESPONDER_PORT = 8888; + // OCSP retry settings constants + private static final String OCSP_MAX_RETRIES = "x509-cert-auth.ocsp-max-retries"; + private static final String OCSP_TIMEOUT_MILLIS = "x509-cert-auth.ocsp-timeout-millis"; + private Undertow ocspResponder; @Drone @@ -95,6 +100,37 @@ public void loginFailedOnOCSPResponderRevocationCheck() throws Exception { assertThat(response.getErrorDescription(), containsString("Certificate's been revoked.")); } + /** + * Get authenticator config by ID + */ + protected AuthenticatorConfigRepresentation getConfig(String configId) { + return authMgmtResource.getAuthenticatorConfig(configId); + } + + @Test + public void testOCSPRetrySettingsConfiguration() throws Exception { + // Test that the OCSP retry settings can be configured + X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel(); + config.setOCSPEnabled(true); + config.setMappingSourceType(SUBJECTDN_EMAIL); + config.setUserIdentityMapperType(USERNAME_EMAIL); + + // Set OCSP retry settings + config.getConfig().put(OCSP_MAX_RETRIES, "5"); + config.getConfig().put(OCSP_TIMEOUT_MILLIS, "3000"); + + AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-retry-config", config.getConfig()); + String cfgId = createConfig(directGrantExecution.getId(), cfg); + Assert.assertNotNull(cfgId); + + // Retrieve the config and verify settings were saved correctly + AuthenticatorConfigRepresentation savedCfg = getConfig(cfgId); + + // Verify the OCSP retry settings were saved correctly + assertEquals("5", savedCfg.getConfig().get(OCSP_MAX_RETRIES)); + assertEquals("3000", savedCfg.getConfig().get(OCSP_TIMEOUT_MILLIS)); + } + @Test public void loginFailedOnOCSPResponderRevocationCheckWithoutCA() throws Exception { X509AuthenticatorConfigModel config = From 021fee75691977950af17ba359155331d447bd34 Mon Sep 17 00:00:00 2001 From: UnicornChance Date: Thu, 11 Sep 2025 21:02:02 +0100 Subject: [PATCH 2/9] refactor retriable client to use cached cclient to be more optimized Signed-off-by: UnicornChance --- .../httpclient/DefaultHttpClientFactory.java | 191 +++++++++--------- 1 file changed, 91 insertions(+), 100 deletions(-) diff --git a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java index b241611aef86..6808145cda58 100755 --- a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java +++ b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java @@ -68,6 +68,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory { public static final String MAX_CONSUMED_RESPONSE_SIZE = "max-consumed-response-size"; private volatile CloseableHttpClient httpClient; + private volatile CloseableHttpClient retriableHttpClient; private Config.Scope config; private BasicResponseHandler stringResponseHandler; @@ -101,112 +102,20 @@ public CloseableHttpClient getHttpClient() { @Override public CloseableHttpClient getRetriableHttpClient() { - return getRetriableHttpClient(defaultRetryConfig); + return retriableHttpClient; } @Override public CloseableHttpClient getRetriableHttpClient(RetryConfig retryConfig) { - // If retries are disabled, just return the default client - if (retryConfig == null || retryConfig.getMaxRetries() <= 0) { - return getHttpClient(); + // If using default config, return the cached client + if (retryConfig == null || + (defaultRetryConfig.getMaxRetries() == retryConfig.getMaxRetries() && + defaultRetryConfig.isRetryOnIOException() == retryConfig.isRetryOnIOException())) { + return retriableHttpClient; } - // Create HTTP client builder - HttpClientBuilder builder = newHttpClientBuilder(); - - // Configure basic settings - long socketTimeout = retryConfig.getSocketTimeoutMillis(); - long establishConnectionTimeout = retryConfig.getConnectionTimeoutMillis(); - int maxPooledPerRoute = config.getInt("max-pooled-per-route", 64); - int connectionPoolSize = config.getInt("connection-pool-size", 128); - long connectionTTL = config.getLong("connection-ttl-millis", -1L); - long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L); - boolean disableCookies = config.getBoolean("disable-cookies", true); - boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false); - boolean reuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true); - - // Configure builder - builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) - .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) - .maxPooledPerRoute(maxPooledPerRoute) - .connectionPoolSize(connectionPoolSize) - .connectionTTL(connectionTTL, TimeUnit.MILLISECONDS) - .maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS) - .disableCookies(disableCookies) - .proxyMappings(DefaultHttpClientFactory.this.configureProxySettings()) - .expectContinueEnabled(expectContinueEnabled) - .reuseConnections(reuseConnections); - - // Configure security settings - DefaultHttpClientFactory.this.configureSecuritySettings(session, builder); - - // Configure retry handler - if (retryConfig.getMaxRetries() > 0) { - builder.getApacheHttpClientBuilder().setRetryHandler( - new org.apache.http.impl.client.DefaultHttpRequestRetryHandler( - retryConfig.getMaxRetries(), retryConfig.isRetryOnIOException()) { - @Override - public boolean retryRequest(IOException exception, int executionCount, - org.apache.http.protocol.HttpContext context) { - boolean shouldRetry = super.retryRequest(exception, executionCount, context); - - if (shouldRetry) { - // Calculate exponential backoff delay with jitter if enabled - long baseDelay = (long) (retryConfig.getInitialBackoffMillis() * - Math.pow(retryConfig.getBackoffMultiplier(), executionCount - 1)); - long delayMillis = calculateBackoffDelay(executionCount, retryConfig); - - // Log retry attempt - if (retryConfig.isUseJitter()) { - logger.debugf( - "Retrying HTTP request (attempt %d) with backoff delay %d ms (base: %d ms, jitter factor: %.2f): %s", - executionCount, delayMillis, baseDelay, - retryConfig.getJitterFactor(), exception.getMessage()); - } else { - logger.debugf( - "Retrying HTTP request (attempt %d) with backoff delay %d ms: %s", - executionCount, delayMillis, exception.getMessage()); - } - - try { - // Sleep for the calculated delay - Thread.sleep(delayMillis); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return false; // Don't retry if interrupted - } - } else if (executionCount > retryConfig.getMaxRetries()) { - logger.debugf("Not retrying HTTP request after %d attempts: %s", - executionCount - 1, exception.getMessage()); - } - return shouldRetry; - } - }); - } - - return builder.build(); - } - - /** - * Calculates the backoff delay with optional jitter - */ - private long calculateBackoffDelay(int executionCount, RetryConfig config) { - // executionCount starts at 1, so we need to subtract 1 to get the retry number - int retryNumber = executionCount - 1; - - // Calculate base exponential backoff: initialBackoff * (multiplier ^ - // retryNumber) - long baseDelay = (long) (config.getInitialBackoffMillis() - * Math.pow(config.getBackoffMultiplier(), retryNumber)); - - // Apply jitter if enabled - if (config.isUseJitter()) { - double jitterFactor = config.getJitterFactor(); - double randomFactor = 1.0 - jitterFactor + (Math.random() * jitterFactor * 2.0); - return (long) (baseDelay * randomFactor); - } else { - return baseDelay; - } + // Otherwise create a new client with the custom config + return DefaultHttpClientFactory.this.createRetriableHttpClient(session, retryConfig); } @Override @@ -265,6 +174,9 @@ public void close() { if (httpClient != null) { httpClient.close(); } + if (retriableHttpClient != null && retriableHttpClient != httpClient) { + retriableHttpClient.close(); + } } catch (IOException ignored) { } @@ -315,11 +227,90 @@ private void lazyInit(KeycloakSession session) { if (httpClient == null) { // Create the default HTTP client with no retries httpClient = createHttpClientWithoutRetries(session); + + // Initialize the default retriable client + if (defaultRetryConfig.getMaxRetries() > 0) { + // Create a retriable client with the default configuration + retriableHttpClient = createRetriableHttpClient(session, defaultRetryConfig); + } else { + // If retries are disabled by default, use the regular client + retriableHttpClient = httpClient; + } } } } } + /** + * Creates a retriable HTTP client with the specified retry configuration + */ + private CloseableHttpClient createRetriableHttpClient(KeycloakSession session, RetryConfig retryConfig) { + // If retries are disabled, just return the default client + if (retryConfig == null || retryConfig.getMaxRetries() <= 0) { + return httpClient; + } + + // Create HTTP client builder + HttpClientBuilder builder = newHttpClientBuilder(); + + // Configure basic settings + long socketTimeout = retryConfig.getSocketTimeoutMillis(); + long establishConnectionTimeout = retryConfig.getConnectionTimeoutMillis(); + int maxPooledPerRoute = config.getInt("max-pooled-per-route", 64); + int connectionPoolSize = config.getInt("connection-pool-size", 128); + long connectionTTL = config.getLong("connection-ttl-millis", -1L); + long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L); + boolean disableCookies = config.getBoolean("disable-cookies", true); + boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false); + boolean reuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true); + + // Configure builder + builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) + .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) + .maxPooledPerRoute(maxPooledPerRoute) + .connectionPoolSize(connectionPoolSize) + .connectionTTL(connectionTTL, TimeUnit.MILLISECONDS) + .maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS) + .disableCookies(disableCookies) + .proxyMappings(configureProxySettings()) + .expectContinueEnabled(expectContinueEnabled) + .reuseConnections(reuseConnections); + + // Configure security settings + configureSecuritySettings(session, builder); + + // Configure retry handler + builder.getApacheHttpClientBuilder().setRetryHandler( + new org.apache.http.impl.client.DefaultHttpRequestRetryHandler( + retryConfig.getMaxRetries(), retryConfig.isRetryOnIOException()) { + @Override + public boolean retryRequest(IOException exception, int executionCount, + org.apache.http.protocol.HttpContext context) { + boolean shouldRetry = super.retryRequest(exception, executionCount, context); + if (shouldRetry) { + try { + // Calculate backoff with jitter + long baseDelay = retryConfig.getInitialBackoffMillis() * + (long)Math.pow(retryConfig.getBackoffMultiplier(), executionCount - 1); + long delay = baseDelay; + if (retryConfig.isUseJitter()) { + // Add +/- 50% jitter + double jitter = 1.0 - retryConfig.getJitterFactor() + + (Math.random() * retryConfig.getJitterFactor() * 2.0); + delay = (long)(baseDelay * jitter); + } + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + return shouldRetry; + } + }); + + return builder.build(); + } + /** * Creates an HTTP client without retry functionality */ From fb072395f6f1c5720cb0519e318a96514b87ca85 Mon Sep 17 00:00:00 2001 From: UnicornChance Date: Fri, 12 Sep 2025 09:33:13 +0100 Subject: [PATCH 3/9] implement equals override to simplify equality of httpclients Signed-off-by: UnicornChance --- .../connections/httpclient/RetryConfig.java | 51 +++++++++++++++++++ .../httpclient/RetryConfigTest.java | 41 +++++++++++++++ .../httpclient/DefaultHttpClientFactory.java | 4 +- 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java index e6717bdf1975..adecf9d71d68 100644 --- a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java +++ b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java @@ -178,6 +178,57 @@ public double getJitterFactor() { return jitterFactor; } + /** + * Compares this RetryConfig with another object for equality. + *

+ * Two RetryConfig objects are considered equal if all their configuration + * parameters match exactly. + * + * @param obj The object to compare with + * @return {@code true} if the objects are equal, {@code false} otherwise + */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + + RetryConfig that = (RetryConfig) obj; + + if (maxRetries != that.maxRetries) return false; + if (retryOnIOException != that.retryOnIOException) return false; + if (initialBackoffMillis != that.initialBackoffMillis) return false; + if (Double.compare(backoffMultiplier, that.backoffMultiplier) != 0) return false; + if (useJitter != that.useJitter) return false; + if (Double.compare(jitterFactor, that.jitterFactor) != 0) return false; + if (connectionTimeoutMillis != that.connectionTimeoutMillis) return false; + return socketTimeoutMillis == that.socketTimeoutMillis; + } + + /** + * Returns a hash code value for this RetryConfig. + *

+ * This method is implemented to be consistent with {@link #equals(Object)}, + * ensuring that equal RetryConfig objects have the same hash code. + * + * @return A hash code value for this object + */ + @Override + public int hashCode() { + int result; + long temp; + result = maxRetries; + result = 31 * result + (retryOnIOException ? 1 : 0); + result = 31 * result + (int) (initialBackoffMillis ^ (initialBackoffMillis >>> 32)); + temp = Double.doubleToLongBits(backoffMultiplier); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + (useJitter ? 1 : 0); + temp = Double.doubleToLongBits(jitterFactor); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + connectionTimeoutMillis; + result = 31 * result + socketTimeoutMillis; + return result; + } + /** * Builder for creating {@link RetryConfig} instances. *

diff --git a/server-spi-private/src/test/java/org/keycloak/connections/httpclient/RetryConfigTest.java b/server-spi-private/src/test/java/org/keycloak/connections/httpclient/RetryConfigTest.java index 89016877da9e..254fbe056596 100644 --- a/server-spi-private/src/test/java/org/keycloak/connections/httpclient/RetryConfigTest.java +++ b/server-spi-private/src/test/java/org/keycloak/connections/httpclient/RetryConfigTest.java @@ -170,4 +170,45 @@ public void testDisableJitter() { assertFalse(config.isUseJitter()); assertEquals(0.5, config.getJitterFactor(), 0.001); // Default value should still be set } + + @Test + public void testEqualsWithSameValues() { + RetryConfig config1 = new RetryConfig.Builder() + .maxRetries(5) + .retryOnIOException(false) + .initialBackoffMillis(2000) + .backoffMultiplier(3.0) + .useJitter(true) + .jitterFactor(0.5) + .connectionTimeoutMillis(15000) + .socketTimeoutMillis(20000) + .build(); + + RetryConfig config2 = new RetryConfig.Builder() + .maxRetries(5) + .retryOnIOException(false) + .initialBackoffMillis(2000) + .backoffMultiplier(3.0) + .useJitter(true) + .jitterFactor(0.5) + .connectionTimeoutMillis(15000) + .socketTimeoutMillis(20000) + .build(); + + assertEquals(config1, config2); + assertEquals(config2, config1); + assertEquals(config1.hashCode(), config2.hashCode()); + } + + @Test + public void testEqualsWithNull() { + RetryConfig config = new RetryConfig.Builder().build(); + assertNotEquals(config, null); + } + + @Test + public void testEqualsSameInstance() { + RetryConfig config = new RetryConfig.Builder().build(); + assertEquals(config, config); + } } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java index 6808145cda58..564f480fc11f 100755 --- a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java +++ b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java @@ -108,9 +108,7 @@ public CloseableHttpClient getRetriableHttpClient() { @Override public CloseableHttpClient getRetriableHttpClient(RetryConfig retryConfig) { // If using default config, return the cached client - if (retryConfig == null || - (defaultRetryConfig.getMaxRetries() == retryConfig.getMaxRetries() && - defaultRetryConfig.isRetryOnIOException() == retryConfig.isRetryOnIOException())) { + if (retryConfig == null || defaultRetryConfig.equals(retryConfig)) { return retriableHttpClient; } From 8a0dfde7e236b7c8c6648f10856e6b119ca39cc4 Mon Sep 17 00:00:00 2001 From: UnicornChance Date: Thu, 16 Oct 2025 13:21:27 -0600 Subject: [PATCH 4/9] Simplify HTTP client retry to server-wide configuration Refactored retry behavior from per-feature configuration to a simpler server-wide approach based on reviewer feedback. This eliminates UI complexity and provides consistent retry behavior across all HTTP clients. Key changes: - Removed OCSP-specific retry configuration from X.509 authenticator UI - Simplified HttpClientProvider interface (removed parameterized method) - Consolidated retry configuration to DefaultHttpClientFactory - Updated documentation to reflect server-wide configuration - Renamed timeout properties to avoid conflicts with existing settings Configuration is now opt-in (max-retries=0 by default) and applies to all outgoing HTTP requests including OCSP validation, identity provider communication, and other external calls. Signed-off-by: UnicornChance --- .../images/x509-ocsp-retry-settings.png | Bin 138019 -> 0 bytes .../topics/authentication/x509.adoc | 16 +--- .../httpclient/HttpClientProvider.java | 36 +-------- .../connections/httpclient/RetryConfig.java | 76 ++++++++---------- ...actX509ClientCertificateAuthenticator.java | 2 - ...ClientCertificateAuthenticatorFactory.java | 16 ---- .../x509/CertificateValidator.java | 61 +------------- .../x509/X509AuthenticatorConfigModel.java | 20 ----- .../httpclient/DefaultHttpClientFactory.java | 49 +++++------ .../DefaultHttpClientFactoryTest.java | 18 ----- .../testsuite/x509/X509OCSPResponderTest.java | 35 -------- 11 files changed, 57 insertions(+), 272 deletions(-) delete mode 100644 docs/documentation/server_admin/images/x509-ocsp-retry-settings.png diff --git a/docs/documentation/server_admin/images/x509-ocsp-retry-settings.png b/docs/documentation/server_admin/images/x509-ocsp-retry-settings.png deleted file mode 100644 index 88d632d3c2ca3b671c7cfe5e5d12ada77d4b2f73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138019 zcmeFYhdW$d*Eoz6i3Eu#5kv`LMwvv7P8enM(R=hx7@Z+Ph!TR)TY@nLqjymv5scnD ziQYvSy?m4VDewJ0zvuf8-f^Al?6cQeyREhNUQM{Vsyr#lBN74v0#Zcle(LWwVk6C0f9nzf-bS1W+!!~A(Z+0Em;1s z3Mt4f?t^G1AoJ~ekf__zZP8rCKPSQ0yCRCAlv5-f<}$_}RvMe|vQODXksIc^vmpfa zo1AlLJ87a<7c1S8E9s&~y$l4a#{Ou2nvv^FYH^@Q(nA{+RaLWCUjo{P-y(>3O_`G1 zvU9!>a(&yH>)LQ2(5OygKp0$YUX_5AIO6UQNM$pt(qM1++`GC(Fbn*M-6LdzFuO=_ znihQDU>c2;CT1?ym{+E9)}D8#YIY7_8vam{9#Z2B> zG)m@c3Gt2-6N(o4-cE8tXm*e8Wjb@`hr5jB0PHAgXjObTdcTuBjJ%zN^2YT7^Y%^_#h2+3?YP{N%d +6;an-;T#e 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 diff --git a/docs/documentation/server_admin/topics/authentication/x509.adoc b/docs/documentation/server_admin/topics/authentication/x509.adoc index e1f4c755b8cc..c7e7db28d519 100644 --- a/docs/documentation/server_admin/topics/authentication/x509.adoc +++ b/docs/documentation/server_admin/topics/authentication/x509.adoc @@ -143,21 +143,7 @@ Checks the certificate revocation status by using Online Certificate Status Prot *OCSP Fail-Open Behavior*:: By default the OCSP check must return a positive response in order to continue with a successful authentication. Sometimes however this check can be inconclusive: for example, the OCSP server could be unreachable, overloaded, or the client certificate may not contain an OCSP responder URI. When this setting is turned ON, authentication will be denied only if an explicit negative response is received by the OCSP responder and the certificate is definitely revoked. If a valid OCSP response is not available the authentication attempt will be accepted. -*OCSP Max Retries*:: -Maximum number of retry attempts for OCSP requests that fail due to network issues. Default is 0 (no retries). Setting this to a value greater than 0 enables retry behavior, which can improve reliability when connecting to OCSP responders over unreliable networks. - -*OCSP Timeout (ms)*:: -Timeout in milliseconds for OCSP requests. This applies to both connection and socket timeouts. Default is 10000 ms (10 seconds). Increase this value if the OCSP responder is slow to respond or decrease it if you need faster failure detection. - -==== OCSP Retry Settings - -. Click *Authentication* in the menu. -. Select Flow. -. Select "X509/Validate Username" or "X509/Validate Username Form" execution settings. -. Configure OCSP Retry Settings. -. Click *Save*. -+ -image:images/x509-ocsp-retry-settings.png[OCSP Retry Settings] +NOTE: OCSP retry behavior is configured server-wide through the HTTP client provider. See the Server Configuration Guide for details on configuring retry settings for all outgoing HTTP requests, including OCSP validation. *OCSP Responder URI*:: Override the value of the OCSP responder URI in the certificate. diff --git a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java index 942e5f62dd89..04b438450655 100755 --- a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java @@ -41,33 +41,19 @@ public interface HttpClientProvider extends Provider { CloseableHttpClient getHttpClient(); /** - * Returns a {@code CloseableHttpClient} with default retry behavior. - * The default retry behavior is configured in the factory. + * Returns a {@code CloseableHttpClient} with server-wide retry behavior. + * The retry behavior is configured globally in the HTTP client factory. *

* The returned {@code HttpClient} instance must never be {@code close()}d by * the caller. *

* - * @return A CloseableHttpClient with retry capabilities + * @return A CloseableHttpClient with retry capabilities based on global configuration */ default CloseableHttpClient getRetriableHttpClient() { return getHttpClient(); // Default implementation for backward compatibility } - /** - * Returns a {@code CloseableHttpClient} with custom retry behavior. - *

- * The returned {@code HttpClient} instance must never be {@code close()}d by - * the caller. - *

- * - * @param retryConfig Configuration for retry behavior - * @return A CloseableHttpClient with retry capabilities - */ - default CloseableHttpClient getRetriableHttpClient(RetryConfig retryConfig) { - return getHttpClient(); - } - /** * Helper method * @@ -131,20 +117,4 @@ default long getMaxConsumedResponseSize() { return DEFAULT_MAX_CONSUMED_RESPONSE_SIZE; } - /** - * Sets a custom retry configuration to be used for subsequent calls to - * getRetriableHttpClient(). - *

- * This method allows setting a retry configuration that will be used for all - * subsequent calls - * to getRetriableHttpClient() that don't explicitly specify a retry - * configuration. - *

- * - * @param retryConfig The retry configuration to use - */ - default void setRetryConfig(RetryConfig retryConfig) { - // Default implementation does nothing - } - } diff --git a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java index adecf9d71d68..7ecbb1c17d7c 100644 --- a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java +++ b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java @@ -22,59 +22,49 @@ *

* This class provides configuration options for HTTP client retry behavior when * making requests. It allows customization of the maximum number of retry - * attempts, - * whether to retry on IO exceptions, exponential backoff settings, jitter for - * backoff times, - * and connection/socket timeouts. + * attempts, whether to retry on IO exceptions, exponential backoff settings, + * jitter for backoff times, and connection/socket timeouts. *

* The default configuration is 0 retry attempts (no retries) with retries - * enabled for IO exceptions if configured, - * with exponential backoff starting at 1000ms and multiplying by 2.0 for each - * retry. - * Jitter is enabled by default with a factor of 0.5 to prevent synchronized - * retry storms. + * enabled for IO exceptions if configured, with exponential backoff starting + * at 1000ms and multiplying by 2.0 for each retry. Jitter is enabled by + * default with a factor of 0.5 to prevent synchronized retry storms. *

- * This configuration is used by the - * {@link HttpClientProvider#getRetriableHttpClient()} - * and {@link HttpClientProvider#getRetriableHttpClient(RetryConfig)} methods to - * create - * HTTP clients with retry capabilities. + * This configuration is used internally by {@link HttpClientProvider#getRetriableHttpClient()} + * to create HTTP clients with server-wide retry capabilities. The configuration + * is set globally through the HTTP client provider SPI configuration. *

- * System properties that can be used to configure retry behavior: + * Server-wide SPI properties for configuring retry behavior: *

    - *
  • {@code max-retries} - Maximum number of retry attempts (default: 0)
  • - *
  • {@code retry-io-exception} - Whether to retry on IO exceptions (default: - * true)
  • - *
  • {@code initial-backoff-millis} - Initial backoff time in milliseconds - * (default: 1000)
  • - *
  • {@code backoff-multiplier} - Multiplier for exponential backoff (default: - * 2.0)
  • - *
  • {@code jitter-factor} - Random jitter factor to apply to backoff times - * (default: 0.5)
  • - *
  • {@code use-jitter} - Whether to apply jitter to backoff times (default: - * true)
  • - *
  • {@code connection-timeout-millis} - Connection timeout in milliseconds - * (default: 10000)
  • - *
  • {@code socket-timeout-millis} - Socket timeout in milliseconds (default: - * 10000)
  • + *
  • {@code spi-connections-http-client-default-max-retries} - Maximum number + * of retry attempts (default: 0)
  • + *
  • {@code spi-connections-http-client-default-retry-on-io-exception} - Whether + * to retry on IO exceptions (default: true)
  • + *
  • {@code spi-connections-http-client-default-initial-backoff-millis} - Initial + * backoff time in milliseconds (default: 1000)
  • + *
  • {@code spi-connections-http-client-default-backoff-multiplier} - Multiplier + * for exponential backoff (default: 2.0)
  • + *
  • {@code spi-connections-http-client-default-jitter-factor} - Random jitter + * factor to apply to backoff times (default: 0.5)
  • + *
  • {@code spi-connections-http-client-default-use-jitter} - Whether to apply + * jitter to backoff times (default: true)
  • + *
  • {@code spi-connections-http-client-default-retry-connection-timeout-millis} - + * Connection timeout in milliseconds for retriable HTTP clients (default: 10000)
  • + *
  • {@code spi-connections-http-client-default-retry-socket-timeout-millis} - Socket + * timeout in milliseconds for retriable HTTP clients (default: 10000)
  • *
*

- * Example usage: + * Example configuration: * *

- * RetryConfig config = new RetryConfig.Builder()
- *         .maxRetries(5)
- *         .retryOnIOException(true)
- *         .initialBackoffMillis(1000)
- *         .backoffMultiplier(2.0)
- *         .useJitter(true)
- *         .jitterFactor(0.5)
- *         .connectionTimeoutMillis(10000)
- *         .socketTimeoutMillis(10000)
- *         .build();
- *
- * CloseableHttpClient client = httpClientProvider.getRetriableHttpClient(config);
+ * spi-connections-http-client-default-max-retries=3
+ * spi-connections-http-client-default-initial-backoff-millis=1000
+ * spi-connections-http-client-default-backoff-multiplier=2.0
  * 
+ * + * This configuration applies to all outgoing HTTP requests from Keycloak, + * including OCSP validation, identity provider communication, and other + * external HTTP calls. */ public class RetryConfig { private final int maxRetries; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java index 7d63ba5fef01..71eff9dd0bb1 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java @@ -60,8 +60,6 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth public static final String ENABLE_CRL = "x509-cert-auth.crl-checking-enabled"; public static final String ENABLE_OCSP = "x509-cert-auth.ocsp-checking-enabled"; public static final String OCSP_FAIL_OPEN = "x509-cert-auth.ocsp-fail-open"; - public static final String OCSP_MAX_RETRIES = "x509-cert-auth.ocsp-max-retries"; - public static final String OCSP_TIMEOUT_MILLIS = "x509-cert-auth.ocsp-timeout-millis"; public static final String ENABLE_CRLDP = "x509-cert-auth.crldp-checking-enabled"; public static final String CANONICAL_DN = "x509-cert-auth.canonical-dn-enabled"; public static final String TIMESTAMP_VALIDATION = "x509-cert-auth.timestamp-validation-enabled"; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java index 2efefb7ec658..1f1b22698f14 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java @@ -195,20 +195,6 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen ocspFailOpen.setHelpText("Whether to allow or deny authentication for client certificates that have missing/invalid/inconclusive OCSP endpoints. By default a successful OCSP response is required."); ocspFailOpen.setLabel("OCSP Fail-Open Behavior"); - ProviderConfigProperty ocspMaxRetries = new ProviderConfigProperty(); - ocspMaxRetries.setType(ProviderConfigProperty.STRING_TYPE); - ocspMaxRetries.setName(OCSP_MAX_RETRIES); - ocspMaxRetries.setDefaultValue("0"); - ocspMaxRetries.setHelpText("Maximum number of retry attempts for OCSP requests that fail due to network issues."); - ocspMaxRetries.setLabel("OCSP Max Retries"); - - ProviderConfigProperty ocspTimeoutMillis = new ProviderConfigProperty(); - ocspTimeoutMillis.setType(ProviderConfigProperty.STRING_TYPE); - ocspTimeoutMillis.setName(OCSP_TIMEOUT_MILLIS); - ocspTimeoutMillis.setDefaultValue("10000"); - ocspTimeoutMillis.setHelpText("Timeout in milliseconds for OCSP requests. This applies to both connection and socket timeouts."); - ocspTimeoutMillis.setLabel("OCSP Timeout (ms)"); - ProviderConfigProperty ocspResponderUri = new ProviderConfigProperty(); ocspResponderUri.setType(STRING_TYPE); ocspResponderUri.setName(OCSPRESPONDER_URI); @@ -274,8 +260,6 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen cRLAbortIfNonUpdated, oCspCheckingEnabled, ocspFailOpen, - ocspMaxRetries, - ocspTimeoutMillis, ocspResponderUri, ocspResponderCert, keyUsage, diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java index 3df83cab6a48..6f9363096703 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java @@ -179,26 +179,11 @@ public static class BouncyCastleOCSPChecker extends OCSPChecker { private final KeycloakSession session; private final String responderUri; private final X509Certificate responderCert; - private final int maxRetries; - private final int timeoutMillis; BouncyCastleOCSPChecker(KeycloakSession session, String responderUri, X509Certificate responderCert) { this.session = session; this.responderUri = responderUri; this.responderCert = responderCert; - - // Default values - this.maxRetries = 0; // No retries by default - this.timeoutMillis = 10000; - } - - BouncyCastleOCSPChecker(KeycloakSession session, String responderUri, X509Certificate responderCert, - int maxRetries, int timeoutMillis) { - this.session = session; - this.responderUri = responderUri; - this.responderCert = responderCert; - this.maxRetries = maxRetries; - this.timeoutMillis = timeoutMillis; } @Override @@ -216,22 +201,6 @@ public OCSPProvider.OCSPRevocationStatus check(X509Certificate cert, X509Certifi // 2) Includes the value of OCSPsigning in ExtendedKeyUsage v3 extension // 3) Certificate is valid at the time OCSPProvider ocspProvider = CryptoIntegration.getProvider().getOCSPProver(OCSPProvider.class); - - // Configure retry behavior - HttpClientProvider httpProvider = session.getProvider(HttpClientProvider.class); - if (httpProvider != null && maxRetries > 0) { - // Use the retriable HTTP client with configured max retries - org.keycloak.connections.httpclient.RetryConfig retryConfig = new org.keycloak.connections.httpclient.RetryConfig.Builder() - .maxRetries(maxRetries) - .connectionTimeoutMillis(timeoutMillis) - .socketTimeoutMillis(timeoutMillis) - .build(); - - httpProvider.setRetryConfig(retryConfig); - logger.debugf("OCSP check configured with maxRetries=%d, timeoutMillis=%d", maxRetries, - timeoutMillis); - } - ocspRevocationStatus = ocspProvider.check(session, cert, issuerCertificate); } else { @@ -245,21 +214,6 @@ public OCSPProvider.OCSPRevocationStatus check(X509Certificate cert, X509Certifi logger.tracef("Responder URI \"%s\" will be used to verify revocation status of the certificate using OCSP with responderCert=%s", uri.toString(), responderCert); - // Configure retry behavior - HttpClientProvider httpProvider = session.getProvider(HttpClientProvider.class); - if (httpProvider != null && maxRetries > 0) { - // Use the retriable HTTP client with configured max retries - org.keycloak.connections.httpclient.RetryConfig retryConfig = new org.keycloak.connections.httpclient.RetryConfig.Builder() - .maxRetries(maxRetries) - .connectionTimeoutMillis(timeoutMillis) - .socketTimeoutMillis(timeoutMillis) - .build(); - - httpProvider.setRetryConfig(retryConfig); - logger.debugf("OCSP check configured with maxRetries=%d, timeoutMillis=%d", maxRetries, - timeoutMillis); - } - // Obtains the revocation status of a certificate using OCSP. // OCSP responder's certificate is assumed to be the issuer's certificate // certificate. @@ -1143,23 +1097,10 @@ public CertificateValidator build(X509Certificate[] certs) { _crlLoader = new CRLFileLoader(session, "", _crlAbortIfNonUpdated); } - // Get OCSP retry settings from X509AuthenticatorConfigModel if available - int ocspMaxRetries = 0; // Default value (no retries) - int ocspTimeoutMillis = 10000; // Default value - - if (session != null) { - X509AuthenticatorConfigModel config = (X509AuthenticatorConfigModel) session - .getAttribute("x509-auth-config"); - if (config != null) { - ocspMaxRetries = config.getOCSPMaxRetries(); - ocspTimeoutMillis = config.getOCSPTimeoutMillis(); - } - } - return new CertificateValidator(certs, _keyUsageBits, _extendedKeyUsage, _certificatePolicy, _certificatePolicyMode, _crlCheckingEnabled, _crlAbortIfNonUpdated, _crldpEnabled, _crlLoader, _ocspEnabled, _ocspFailOpen, - new BouncyCastleOCSPChecker(session, _responderUri, _responderCert, ocspMaxRetries, ocspTimeoutMillis), session, _timestampValidationEnabled, _trustValidationEnabled); + new BouncyCastleOCSPChecker(session, _responderUri, _responderCert), session, _timestampValidationEnabled, _trustValidationEnabled); } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java index ab676e0c0696..7fe0106a9999 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java @@ -139,26 +139,6 @@ public X509AuthenticatorConfigModel setOCSPFailOpen(boolean value) { return this; } - public int getOCSPMaxRetries() { - String value = getConfig().get(OCSP_MAX_RETRIES); - return value != null ? Integer.parseInt(value) : 0; // Default to 0 retries (no retry) - } - - public X509AuthenticatorConfigModel setOCSPMaxRetries(int value) { - getConfig().put(OCSP_MAX_RETRIES, Integer.toString(value)); - return this; - } - - public int getOCSPTimeoutMillis() { - String value = getConfig().get(OCSP_TIMEOUT_MILLIS); - return value != null ? Integer.parseInt(value) : 10000; // Default to 10 seconds - } - - public X509AuthenticatorConfigModel setOCSPTimeoutMillis(int value) { - getConfig().put(OCSP_TIMEOUT_MILLIS, Integer.toString(value)); - return this; - } - public boolean getCRLDistributionPointEnabled() { return Boolean.parseBoolean(getConfig().get(ENABLE_CRLDP)); } diff --git a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java index 564f480fc11f..2dc2ef6a33c4 100755 --- a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java +++ b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java @@ -105,17 +105,6 @@ public CloseableHttpClient getRetriableHttpClient() { return retriableHttpClient; } - @Override - public CloseableHttpClient getRetriableHttpClient(RetryConfig retryConfig) { - // If using default config, return the cached client - if (retryConfig == null || defaultRetryConfig.equals(retryConfig)) { - return retriableHttpClient; - } - - // Otherwise create a new client with the custom config - return DefaultHttpClientFactory.this.createRetriableHttpClient(session, retryConfig); - } - @Override public void close() { @@ -189,23 +178,23 @@ public String getId() { public void init(Config.Scope config) { this.config = config; - // Initialize default retry configuration - int maxRetries = config.getInt("http-client.default-max-retries", 0); // No retries by default - boolean retryOnIOException = config.getBoolean("http-client.default-retry-on-io-exception", true); - long initialBackoffMillis = config.getLong("http-client.default-initial-backoff-millis", 1000L); + // Initialize server-wide retry configuration + int maxRetries = config.getInt("max-retries", 0); // No retries by default (opt-in) + boolean retryOnIOException = config.getBoolean("retry-on-io-exception", true); + long initialBackoffMillis = config.getLong("initial-backoff-millis", 1000L); // Get backoff multiplier as a string and convert to double (Config.Scope // doesn't have getDouble) - String backoffMultiplierStr = config.get("http-client.default-backoff-multiplier", "2.0"); + String backoffMultiplierStr = config.get("backoff-multiplier", "2.0"); double backoffMultiplier = Double.parseDouble(backoffMultiplierStr); // Get jitter settings - boolean useJitter = config.getBoolean("http-client.default-use-jitter", true); - String jitterFactorStr = config.get("http-client.default-jitter-factor", "0.5"); + boolean useJitter = config.getBoolean("use-jitter", true); + String jitterFactorStr = config.get("jitter-factor", "0.5"); double jitterFactor = Double.parseDouble(jitterFactorStr); - int connectionTimeoutMillis = config.getInt("http-client.default-connection-timeout-millis", 10000); - int socketTimeoutMillis = config.getInt("http-client.default-socket-timeout-millis", 10000); + int connectionTimeoutMillis = config.getInt("retry-connection-timeout-millis", 10000); + int socketTimeoutMillis = config.getInt("retry-socket-timeout-millis", 10000); defaultRetryConfig = new RetryConfig.Builder() .maxRetries(maxRetries) @@ -505,52 +494,52 @@ public List getConfigMetadata() { .defaultValue(HttpClientProvider.DEFAULT_MAX_CONSUMED_RESPONSE_SIZE) .add() .property() - .name("http-client.default-max-retries") + .name("max-retries") .type("int") - .helpText("Maximum number of retry attempts for HTTP requests.") - .defaultValue(3) + .helpText("Maximum number of retry attempts for all outgoing HTTP requests. Set to 0 to disable retries (default).") + .defaultValue(0) .add() .property() - .name("http-client.default-retry-on-io-exception") + .name("retry-on-io-exception") .type("boolean") .helpText("Whether to retry HTTP requests on IO exceptions.") .defaultValue(true) .add() .property() - .name("http-client.default-initial-backoff-millis") + .name("initial-backoff-millis") .type("long") .helpText("Initial backoff time in milliseconds before the first retry attempt.") .defaultValue(1000L) .add() .property() - .name("http-client.default-backoff-multiplier") + .name("backoff-multiplier") .type("string") .helpText( "Multiplier for exponential backoff between retry attempts. For example, with an initial backoff of 1000ms and a multiplier of 2.0, the retry delays would be: 1000ms, 2000ms, 4000ms, etc.") .defaultValue("2.0") .add() .property() - .name("http-client.default-use-jitter") + .name("use-jitter") .type("boolean") .helpText( "Whether to apply jitter to backoff times to prevent synchronized retry storms when multiple clients are retrying at the same time.") .defaultValue(true) .add() .property() - .name("http-client.default-jitter-factor") + .name("jitter-factor") .type("string") .helpText( "Jitter factor to apply to backoff times. A value of 0.5 means the actual backoff time will be between 50% and 150% of the calculated exponential backoff time.") .defaultValue("0.5") .add() .property() - .name("http-client.default-connection-timeout-millis") + .name("retry-connection-timeout-millis") .type("int") .helpText("Connection timeout in milliseconds for retriable HTTP clients.") .defaultValue(10000) .add() .property() - .name("http-client.default-socket-timeout-millis") + .name("retry-socket-timeout-millis") .type("int") .helpText("Socket timeout in milliseconds for retriable HTTP clients.") .defaultValue(10000) diff --git a/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java b/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java index 8f4934b6722f..491e7f160373 100644 --- a/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java +++ b/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java @@ -108,24 +108,6 @@ public void testGetRetriableHttpClient() throws IOException { org.junit.Assert.assertNotNull("Retriable HTTP client should not be null", client); } - @Test - public void testGetRetriableHttpClientWithCustomConfig() throws IOException { - // Create provider with default settings - HttpClientProvider provider = createDefaultProvider(); - - // Create custom retry config - RetryConfig config = new RetryConfig.Builder() - .maxRetries(5) - .retryOnIOException(false) - .build(); - - // Get retriable HTTP client with custom config - CloseableHttpClient client = provider.getRetriableHttpClient(config); - - // Verify client is not null - org.junit.Assert.assertNotNull("Retriable HTTP client with custom config should not be null", client); - } - @Test public void testFactoryInitWithRetryProperties() { // Create factory with custom retry properties diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java index 53d99cd76fa8..4a6efdb2c0b1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java @@ -65,10 +65,6 @@ public class X509OCSPResponderTest extends AbstractX509AuthenticationTest { private static final int OCSP_RESPONDER_PORT = 8888; - // OCSP retry settings constants - private static final String OCSP_MAX_RETRIES = "x509-cert-auth.ocsp-max-retries"; - private static final String OCSP_TIMEOUT_MILLIS = "x509-cert-auth.ocsp-timeout-millis"; - private Undertow ocspResponder; @Drone @@ -100,37 +96,6 @@ public void loginFailedOnOCSPResponderRevocationCheck() throws Exception { assertThat(response.getErrorDescription(), containsString("Certificate's been revoked.")); } - /** - * Get authenticator config by ID - */ - protected AuthenticatorConfigRepresentation getConfig(String configId) { - return authMgmtResource.getAuthenticatorConfig(configId); - } - - @Test - public void testOCSPRetrySettingsConfiguration() throws Exception { - // Test that the OCSP retry settings can be configured - X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel(); - config.setOCSPEnabled(true); - config.setMappingSourceType(SUBJECTDN_EMAIL); - config.setUserIdentityMapperType(USERNAME_EMAIL); - - // Set OCSP retry settings - config.getConfig().put(OCSP_MAX_RETRIES, "5"); - config.getConfig().put(OCSP_TIMEOUT_MILLIS, "3000"); - - AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-retry-config", config.getConfig()); - String cfgId = createConfig(directGrantExecution.getId(), cfg); - Assert.assertNotNull(cfgId); - - // Retrieve the config and verify settings were saved correctly - AuthenticatorConfigRepresentation savedCfg = getConfig(cfgId); - - // Verify the OCSP retry settings were saved correctly - assertEquals("5", savedCfg.getConfig().get(OCSP_MAX_RETRIES)); - assertEquals("3000", savedCfg.getConfig().get(OCSP_TIMEOUT_MILLIS)); - } - @Test public void loginFailedOnOCSPResponderRevocationCheckWithoutCA() throws Exception { X509AuthenticatorConfigModel config = From dc87a7f59b0ec9dec6774cdbe2df05e88bd12c50 Mon Sep 17 00:00:00 2001 From: UnicornChance Date: Fri, 17 Oct 2025 07:28:42 -0600 Subject: [PATCH 5/9] Simplify HTTP client retry to single client with server-wide configuration Refactored HTTP client retry implementation based on reviewer feedback to use a single HTTP client with built-in retry behavior instead of separate retriable and non-retriable clients. Signed-off-by: UnicornChance --- .../topics/authentication/x509.adoc | 2 +- .../httpclient/HttpClientProvider.java | 14 - .../connections/httpclient/RetryConfig.java | 10 +- .../java/org/keycloak/utils/OCSPProvider.java | 7 +- ...actX509ClientCertificateAuthenticator.java | 11 +- ...ClientCertificateAuthenticatorFactory.java | 1 + .../x509/CertificateValidator.java | 3 +- .../httpclient/DefaultHttpClientFactory.java | 303 ++++++------------ .../DefaultHttpClientFactoryTest.java | 24 +- .../testsuite/x509/X509OCSPResponderTest.java | 1 - 10 files changed, 118 insertions(+), 258 deletions(-) diff --git a/docs/documentation/server_admin/topics/authentication/x509.adoc b/docs/documentation/server_admin/topics/authentication/x509.adoc index c7e7db28d519..2062da402408 100644 --- a/docs/documentation/server_admin/topics/authentication/x509.adoc +++ b/docs/documentation/server_admin/topics/authentication/x509.adoc @@ -143,7 +143,7 @@ Checks the certificate revocation status by using Online Certificate Status Prot *OCSP Fail-Open Behavior*:: By default the OCSP check must return a positive response in order to continue with a successful authentication. Sometimes however this check can be inconclusive: for example, the OCSP server could be unreachable, overloaded, or the client certificate may not contain an OCSP responder URI. When this setting is turned ON, authentication will be denied only if an explicit negative response is received by the OCSP responder and the certificate is definitely revoked. If a valid OCSP response is not available the authentication attempt will be accepted. -NOTE: OCSP retry behavior is configured server-wide through the HTTP client provider. See the Server Configuration Guide for details on configuring retry settings for all outgoing HTTP requests, including OCSP validation. +OCSP retry behavior is configured server-wide through the HTTP client provider. See the Server Configuration Guide for details on configuring retry settings for all outgoing HTTP requests, including OCSP validation. *OCSP Responder URI*:: Override the value of the OCSP responder URI in the certificate. diff --git a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java index 04b438450655..c0169c776aff 100755 --- a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java @@ -40,20 +40,6 @@ public interface HttpClientProvider extends Provider { */ CloseableHttpClient getHttpClient(); - /** - * Returns a {@code CloseableHttpClient} with server-wide retry behavior. - * The retry behavior is configured globally in the HTTP client factory. - *

- * The returned {@code HttpClient} instance must never be {@code close()}d by - * the caller. - *

- * - * @return A CloseableHttpClient with retry capabilities based on global configuration - */ - default CloseableHttpClient getRetriableHttpClient() { - return getHttpClient(); // Default implementation for backward compatibility - } - /** * Helper method * diff --git a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java index 7ecbb1c17d7c..f13732490904 100644 --- a/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java +++ b/server-spi-private/src/main/java/org/keycloak/connections/httpclient/RetryConfig.java @@ -30,7 +30,7 @@ * at 1000ms and multiplying by 2.0 for each retry. Jitter is enabled by * default with a factor of 0.5 to prevent synchronized retry storms. *

- * This configuration is used internally by {@link HttpClientProvider#getRetriableHttpClient()} + * This configuration is used internally by {@link HttpClientProvider#getHttpClient()} * to create HTTP clients with server-wide retry capabilities. The configuration * is set globally through the HTTP client provider SPI configuration. *

@@ -38,8 +38,8 @@ *

    *
  • {@code spi-connections-http-client-default-max-retries} - Maximum number * of retry attempts (default: 0)
  • - *
  • {@code spi-connections-http-client-default-retry-on-io-exception} - Whether - * to retry on IO exceptions (default: true)
  • + *
  • {@code spi-connections-http-client-default-retry-on-error} - Whether + * to retry on errors (default: true)
  • *
  • {@code spi-connections-http-client-default-initial-backoff-millis} - Initial * backoff time in milliseconds (default: 1000)
  • *
  • {@code spi-connections-http-client-default-backoff-multiplier} - Multiplier @@ -48,10 +48,6 @@ * factor to apply to backoff times (default: 0.5)
  • *
  • {@code spi-connections-http-client-default-use-jitter} - Whether to apply * jitter to backoff times (default: true)
  • - *
  • {@code spi-connections-http-client-default-retry-connection-timeout-millis} - - * Connection timeout in milliseconds for retriable HTTP clients (default: 10000)
  • - *
  • {@code spi-connections-http-client-default-retry-socket-timeout-millis} - Socket - * timeout in milliseconds for retriable HTTP clients (default: 10000)
  • *
*

* Example configuration: diff --git a/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java b/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java index cc82b48bb455..92d79d35e2a7 100644 --- a/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java @@ -40,6 +40,7 @@ import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.models.KeycloakSession; + /** * @author Peter Nalyvayko * @version $Revision: 1 $ @@ -85,7 +86,6 @@ public OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, return check(session, cert, issuerCertificate, Collections.singletonList(responderURI), responderCert, date); } - /** * Requests certificate revocation status using OCSP. The OCSP responder URI * is obtained from the certificate's AIA extension. @@ -122,8 +122,7 @@ public OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, protected byte[] getEncodedOCSPResponse(KeycloakSession session, byte[] encodedOCSPReq, URI responderUri) throws IOException { - // Use the retriable HTTP client which will apply retry and timeout settings from RetryConfig - CloseableHttpClient httpClient = session.getProvider(HttpClientProvider.class).getRetriableHttpClient(); + CloseableHttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient(); HttpPost post = new HttpPost(responderUri); post.setHeader(HttpHeaders.CONTENT_TYPE, "application/ocsp-request"); post.setEntity(new ByteArrayEntity(encodedOCSPReq)); @@ -173,6 +172,7 @@ protected abstract OCSPRevocationStatus check(KeycloakSession session, X509Certi X509Certificate issuerCertificate, List responderURIs, X509Certificate responderCert, Date date) throws CertPathValidatorException; + protected static OCSPRevocationStatus unknownStatus() { return new OCSPRevocationStatus() { @Override @@ -201,4 +201,5 @@ public CRLReason getRevocationReason() { */ protected abstract List getResponderURIs(X509Certificate cert) throws CertificateEncodingException; + } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java index 71eff9dd0bb1..7f4886f67ffb 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java @@ -46,6 +46,7 @@ import org.keycloak.models.UserModel; import org.keycloak.services.x509.X509ClientCertificateLookup; + /** * @author Peter Nalyvayko * @version $Revision: 1 $ @@ -105,12 +106,6 @@ protected static class CertificateValidatorConfigBuilder { static CertificateValidator.CertificateValidatorBuilder fromConfig(KeycloakSession session, X509AuthenticatorConfigModel config) throws Exception { CertificateValidator.CertificateValidatorBuilder builder = new CertificateValidator.CertificateValidatorBuilder(); - - // Store the config in the session so it can be accessed by the - // CertificateValidator - // This allows passing the OCSP retry settings to the validator - session.setAttribute("x509-auth-config", config); - return builder .session(session) .keyUsage() @@ -276,6 +271,7 @@ protected X509Certificate[] getCertificateChain(AuthenticationFlowContext contex return null; } + // Saving some notes for audit to authSession as the event may not be necessarily triggered in this HTTP request where the certificate was parsed // For example if there is confirmation page enabled, it will be in the additional request protected void saveX509CertificateAuditDataToAuthSession(AuthenticationFlowContext context, @@ -296,16 +292,15 @@ private void recordX509DetailFromAuthSessionToEvent(AuthenticationFlowContext co context.getEvent().detail(detailName, detailValue); } + // Purely for unit testing public UserIdentityExtractor getUserIdentityExtractor(X509AuthenticatorConfigModel config) { return UserIdentityExtractorBuilder.fromConfig(config); } - // Purely for unit testing public UserIdentityToModelMapper getUserIdentityToModelMapper(X509AuthenticatorConfigModel config) { return UserIdentityToModelMapperBuilder.fromConfig(config); } - @Override public boolean requiresUser() { return false; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java index 1f1b22698f14..9828f9c609d9 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java @@ -115,6 +115,7 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen serialnumberHex.setDefaultValue(Boolean.toString(false)); serialnumberHex.setHelpText("Use the hex representation of the serial number. This option is relevant for authenticators using serial number."); + ProviderConfigProperty regExp = new ProviderConfigProperty(); regExp.setType(STRING_TYPE); regExp.setName(REGULAR_EXPRESSION); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java index 6f9363096703..8275255b8c71 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java @@ -213,7 +213,6 @@ public OCSPProvider.OCSPRevocationStatus check(X509Certificate cert, X509Certifi } logger.tracef("Responder URI \"%s\" will be used to verify revocation status of the certificate using OCSP with responderCert=%s", uri.toString(), responderCert); - // Obtains the revocation status of a certificate using OCSP. // OCSP responder's certificate is assumed to be the issuer's certificate // certificate. @@ -1096,7 +1095,6 @@ public CertificateValidator build(X509Certificate[] certs) { if (_crlLoader == null) { _crlLoader = new CRLFileLoader(session, "", _crlAbortIfNonUpdated); } - return new CertificateValidator(certs, _keyUsageBits, _extendedKeyUsage, _certificatePolicy, _certificatePolicyMode, _crlCheckingEnabled, _crlAbortIfNonUpdated, _crldpEnabled, _crlLoader, _ocspEnabled, _ocspFailOpen, @@ -1104,4 +1102,5 @@ public CertificateValidator build(X509Certificate[] certs) { } } + } diff --git a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java index 2dc2ef6a33c4..0670aafa98f0 100755 --- a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java +++ b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java @@ -68,7 +68,6 @@ public class DefaultHttpClientFactory implements HttpClientFactory { public static final String MAX_CONSUMED_RESPONSE_SIZE = "max-consumed-response-size"; private volatile CloseableHttpClient httpClient; - private volatile CloseableHttpClient retriableHttpClient; private Config.Scope config; private BasicResponseHandler stringResponseHandler; @@ -76,9 +75,6 @@ public class DefaultHttpClientFactory implements HttpClientFactory { private final InputStreamResponseHandler inputStreamResponseHandler = new InputStreamResponseHandler(); private long maxConsumedResponseSize; - // Retry configuration - private RetryConfig defaultRetryConfig; - private static class InputStreamResponseHandler extends AbstractResponseHandler { public InputStream handleEntity(HttpEntity entity) throws IOException { @@ -100,11 +96,6 @@ public CloseableHttpClient getHttpClient() { return httpClient; } - @Override - public CloseableHttpClient getRetriableHttpClient() { - return retriableHttpClient; - } - @Override public void close() { @@ -161,9 +152,6 @@ public void close() { if (httpClient != null) { httpClient.close(); } - if (retriableHttpClient != null && retriableHttpClient != httpClient) { - retriableHttpClient.close(); - } } catch (IOException ignored) { } @@ -177,113 +165,122 @@ public String getId() { @Override public void init(Config.Scope config) { this.config = config; - - // Initialize server-wide retry configuration - int maxRetries = config.getInt("max-retries", 0); // No retries by default (opt-in) - boolean retryOnIOException = config.getBoolean("retry-on-io-exception", true); - long initialBackoffMillis = config.getLong("initial-backoff-millis", 1000L); - - // Get backoff multiplier as a string and convert to double (Config.Scope - // doesn't have getDouble) - String backoffMultiplierStr = config.get("backoff-multiplier", "2.0"); - double backoffMultiplier = Double.parseDouble(backoffMultiplierStr); - - // Get jitter settings - boolean useJitter = config.getBoolean("use-jitter", true); - String jitterFactorStr = config.get("jitter-factor", "0.5"); - double jitterFactor = Double.parseDouble(jitterFactorStr); - - int connectionTimeoutMillis = config.getInt("retry-connection-timeout-millis", 10000); - int socketTimeoutMillis = config.getInt("retry-socket-timeout-millis", 10000); - - defaultRetryConfig = new RetryConfig.Builder() - .maxRetries(maxRetries) - .retryOnIOException(retryOnIOException) - .initialBackoffMillis(initialBackoffMillis) - .backoffMultiplier(backoffMultiplier) - .useJitter(useJitter) - .jitterFactor(jitterFactor) - .connectionTimeoutMillis(connectionTimeoutMillis) - .socketTimeoutMillis(socketTimeoutMillis) - .build(); } private void lazyInit(KeycloakSession session) { if (httpClient == null) { synchronized(this) { if (httpClient == null) { - // Create the default HTTP client with no retries - httpClient = createHttpClientWithoutRetries(session); + long socketTimeout = config.getLong("socket-timeout-millis", 5000L); + long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L); + int maxPooledPerRoute = config.getInt("max-pooled-per-route", 64); + int connectionPoolSize = config.getInt("connection-pool-size", 128); + long connectionTTL = config.getLong("connection-ttl-millis", -1L); + long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L); + boolean disableCookies = config.getBoolean("disable-cookies", true); + String clientKeystore = config.get("client-keystore"); + String clientKeystorePassword = config.get("client-keystore-password"); + String clientPrivateKeyPassword = config.get("client-key-password"); + boolean disableTrustManager = config.getBoolean("disable-trust-manager", false); + boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false); + boolean reuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true); + + ProxyMappings proxyMappings = ProxyMappings.valueOf(config.getArray("proxy-mappings")); + if (proxyMappings == null || proxyMappings.isEmpty()) { + logger.debug("Trying to use proxy mapping from env vars"); + String httpProxy = getEnvVarValue(HTTPS_PROXY); + if (isBlank(httpProxy)) { + httpProxy = getEnvVarValue(HTTP_PROXY); + } + String noProxy = getEnvVarValue(NO_PROXY); + logger.debugf("httpProxy: %s, noProxy: %s", httpProxy, noProxy); + proxyMappings = ProxyMappings.withFixedProxyMapping(httpProxy, noProxy); + } - // Initialize the default retriable client - if (defaultRetryConfig.getMaxRetries() > 0) { - // Create a retriable client with the default configuration - retriableHttpClient = createRetriableHttpClient(session, defaultRetryConfig); + HttpClientBuilder builder = newHttpClientBuilder(); + + builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) + .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) + .maxPooledPerRoute(maxPooledPerRoute) + .connectionPoolSize(connectionPoolSize) + .connectionTTL(connectionTTL, TimeUnit.MILLISECONDS) + .maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS) + .disableCookies(disableCookies) + .proxyMappings(proxyMappings) + .expectContinueEnabled(expectContinueEnabled) + .reuseConnections(reuseConnections); + + TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class); + boolean disableTruststoreProvider = truststoreProvider == null || truststoreProvider.getTruststore() == null; + if (disableTruststoreProvider) { + logger.warn("TruststoreProvider is disabled"); } else { - // If retries are disabled by default, use the regular client - retriableHttpClient = httpClient; + builder.hostnameVerification(truststoreProvider.getPolicy()); + try { + builder.trustStore(truststoreProvider.getTruststore()); + } catch (Exception e) { + throw new RuntimeException("Failed to load truststore", e); + } } + + if (disableTrustManager) { + logger.warn("TrustManager is disabled"); + builder.disableTrustManager(); + } + + if (clientKeystore != null) { + clientKeystore = EnvUtil.replace(clientKeystore); + try { + KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword); + builder.keyStore(clientCertKeystore, clientPrivateKeyPassword); + } catch (Exception e) { + throw new RuntimeException("Failed to load keystore", e); + } + } + + // Configure retry behavior + configureRetries(builder); + + httpClient = builder.build(); } } } } /** - * Creates a retriable HTTP client with the specified retry configuration + * Configures retry behavior for the HTTP client builder. + * Applies server-wide retry configuration if enabled. + * + * @param builder The HTTP client builder to configure */ - private CloseableHttpClient createRetriableHttpClient(KeycloakSession session, RetryConfig retryConfig) { - // If retries are disabled, just return the default client - if (retryConfig == null || retryConfig.getMaxRetries() <= 0) { - return httpClient; + private void configureRetries(HttpClientBuilder builder) { + int maxRetries = config.getInt("max-retries", 0); + if (maxRetries <= 0) { + return; // Retries disabled } - // Create HTTP client builder - HttpClientBuilder builder = newHttpClientBuilder(); - - // Configure basic settings - long socketTimeout = retryConfig.getSocketTimeoutMillis(); - long establishConnectionTimeout = retryConfig.getConnectionTimeoutMillis(); - int maxPooledPerRoute = config.getInt("max-pooled-per-route", 64); - int connectionPoolSize = config.getInt("connection-pool-size", 128); - long connectionTTL = config.getLong("connection-ttl-millis", -1L); - long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L); - boolean disableCookies = config.getBoolean("disable-cookies", true); - boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false); - boolean reuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true); - - // Configure builder - builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) - .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) - .maxPooledPerRoute(maxPooledPerRoute) - .connectionPoolSize(connectionPoolSize) - .connectionTTL(connectionTTL, TimeUnit.MILLISECONDS) - .maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS) - .disableCookies(disableCookies) - .proxyMappings(configureProxySettings()) - .expectContinueEnabled(expectContinueEnabled) - .reuseConnections(reuseConnections); - - // Configure security settings - configureSecuritySettings(session, builder); - - // Configure retry handler + boolean retryOnError = config.getBoolean("retry-on-error", true); + long initialBackoffMillis = config.getLong("initial-backoff-millis", 1000L); + String backoffMultiplierStr = config.get("backoff-multiplier", "2.0"); + double backoffMultiplier = Double.parseDouble(backoffMultiplierStr); + boolean useJitter = config.getBoolean("use-jitter", true); + String jitterFactorStr = config.get("jitter-factor", "0.5"); + double jitterFactor = Double.parseDouble(jitterFactorStr); + builder.getApacheHttpClientBuilder().setRetryHandler( - new org.apache.http.impl.client.DefaultHttpRequestRetryHandler( - retryConfig.getMaxRetries(), retryConfig.isRetryOnIOException()) { + new org.apache.http.impl.client.DefaultHttpRequestRetryHandler(maxRetries, retryOnError) { @Override public boolean retryRequest(IOException exception, int executionCount, org.apache.http.protocol.HttpContext context) { boolean shouldRetry = super.retryRequest(exception, executionCount, context); if (shouldRetry) { try { - // Calculate backoff with jitter - long baseDelay = retryConfig.getInitialBackoffMillis() * - (long)Math.pow(retryConfig.getBackoffMultiplier(), executionCount - 1); + long baseDelay = initialBackoffMillis * + (long)Math.pow(backoffMultiplier, executionCount - 1); long delay = baseDelay; - if (retryConfig.isUseJitter()) { - // Add +/- 50% jitter - double jitter = 1.0 - retryConfig.getJitterFactor() + - (Math.random() * retryConfig.getJitterFactor() * 2.0); + if (useJitter) { + double jitter = 1.0 - jitterFactor + + (Math.random() * jitterFactor * 2.0); delay = (long)(baseDelay * jitter); } Thread.sleep(delay); @@ -294,110 +291,6 @@ public boolean retryRequest(IOException exception, int executionCount, return shouldRetry; } }); - - return builder.build(); - } - - /** - * Creates an HTTP client without retry functionality - */ - private CloseableHttpClient createHttpClientWithoutRetries(KeycloakSession session) { - // Create HTTP client builder - HttpClientBuilder builder = newHttpClientBuilder(); - - // Configure basic settings - long socketTimeout = config.getLong("socket-timeout-millis", 5000L); - long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L); - int maxPooledPerRoute = config.getInt("max-pooled-per-route", 64); - int connectionPoolSize = config.getInt("connection-pool-size", 128); - long connectionTTL = config.getLong("connection-ttl-millis", -1L); - long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L); - boolean disableCookies = config.getBoolean("disable-cookies", true); - boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false); - boolean reuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true); - - // Configure builder - builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) - .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) - .maxPooledPerRoute(maxPooledPerRoute) - .connectionPoolSize(connectionPoolSize) - .connectionTTL(connectionTTL, TimeUnit.MILLISECONDS) - .maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS) - .disableCookies(disableCookies) - .proxyMappings(configureProxySettings()) - .expectContinueEnabled(expectContinueEnabled) - .reuseConnections(reuseConnections); - - // Configure security settings - configureSecuritySettings(session, builder); - - // Build the client - return builder.build(); - } - - /** - * Configures security settings for an HTTP client builder. - * - * @param session The Keycloak session - * @param builder The HTTP client builder to configure - */ - private void configureSecuritySettings(KeycloakSession session, HttpClientBuilder builder) { - // Configure TrustStore - TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class); - boolean disableTruststoreProvider = truststoreProvider == null || truststoreProvider.getTruststore() == null; - - if (disableTruststoreProvider) { - logger.warn("TruststoreProvider is disabled"); - } else { - builder.hostnameVerification(truststoreProvider.getPolicy()); - try { - builder.trustStore(truststoreProvider.getTruststore()); - } catch (Exception e) { - throw new RuntimeException("Failed to load truststore", e); - } - } - - // Configure TrustManager - boolean disableTrustManager = config.getBoolean("disable-trust-manager", false); - if (disableTrustManager) { - logger.warn("TrustManager is disabled"); - builder.disableTrustManager(); - } - - // Configure KeyStore - String clientKeystore = config.get("client-keystore"); - if (clientKeystore != null) { - clientKeystore = EnvUtil.replace(clientKeystore); - String clientKeystorePassword = config.get("client-keystore-password"); - String clientPrivateKeyPassword = config.get("client-key-password"); - try { - KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword); - builder.keyStore(clientCertKeystore, clientPrivateKeyPassword); - } catch (Exception e) { - throw new RuntimeException("Failed to load keystore", e); - } - } - } - - /** - * Configures proxy settings for HTTP clients. - * - * @return ProxyMappings configured based on settings or environment variables - */ - private ProxyMappings configureProxySettings() { - ProxyMappings proxyMappings = ProxyMappings.valueOf(config.getArray("proxy-mappings")); - if (proxyMappings == null || proxyMappings.isEmpty()) { - logger.debug("Trying to use proxy mapping from env vars"); - String httpProxy = getEnvVarValue(HTTPS_PROXY); - if (isBlank(httpProxy)) { - httpProxy = getEnvVarValue(HTTP_PROXY); - } - String noProxy = getEnvVarValue(NO_PROXY); - - logger.debugf("httpProxy: %s, noProxy: %s", httpProxy, noProxy); - proxyMappings = ProxyMappings.withFixedProxyMapping(httpProxy, noProxy); - } - return proxyMappings; } protected HttpClientBuilder newHttpClientBuilder() { @@ -500,9 +393,9 @@ public List getConfigMetadata() { .defaultValue(0) .add() .property() - .name("retry-on-io-exception") + .name("retry-on-error") .type("boolean") - .helpText("Whether to retry HTTP requests on IO exceptions.") + .helpText("Whether to retry HTTP requests on errors.") .defaultValue(true) .add() .property() @@ -532,18 +425,6 @@ public List getConfigMetadata() { "Jitter factor to apply to backoff times. A value of 0.5 means the actual backoff time will be between 50% and 150% of the calculated exponential backoff time.") .defaultValue("0.5") .add() - .property() - .name("retry-connection-timeout-millis") - .type("int") - .helpText("Connection timeout in milliseconds for retriable HTTP clients.") - .defaultValue(10000) - .add() - .property() - .name("retry-socket-timeout-millis") - .type("int") - .helpText("Socket timeout in milliseconds for retriable HTTP clients.") - .defaultValue(10000) - .add() .build(); } diff --git a/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java b/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java index 491e7f160373..086fadc3a6cb 100644 --- a/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java +++ b/services/src/test/java/org/keycloak/connections/httpclient/DefaultHttpClientFactoryTest.java @@ -43,7 +43,7 @@ public class DefaultHttpClientFactoryTest { private static final String DISABLE_TRUST_MANAGER_PROPERTY = "disable-trust-manager"; private static final String TEST_DOMAIN = "keycloak.org"; private static final String MAX_RETRIES_PROPERTY = "max-retries"; - private static final String RETRY_IO_EXCEPTION_PROPERTY = "retry-io-exception"; + private static final String RETRY_ON_ERROR_PROPERTY = "retry-on-error"; // Common objects for tests private DefaultHttpClientFactory factory; @@ -97,15 +97,17 @@ public void createHttpClientProviderWithUnvailableURL() throws IOException { } @Test - public void testGetRetriableHttpClient() throws IOException { - // Create provider with default retry config - HttpClientProvider provider = createDefaultProvider(); + public void testGetHttpClientWithRetries() throws IOException { + // Create provider with retry config + Map values = new HashMap<>(); + values.put(MAX_RETRIES_PROPERTY, "3"); + HttpClientProvider provider = createProviderWithProperties(values); - // Get retriable HTTP client with default config - CloseableHttpClient client = provider.getRetriableHttpClient(); + // Get HTTP client (now has retry built-in) + CloseableHttpClient client = provider.getHttpClient(); // Verify client is not null - org.junit.Assert.assertNotNull("Retriable HTTP client should not be null", client); + org.junit.Assert.assertNotNull("HTTP client should not be null", client); } @Test @@ -113,16 +115,16 @@ public void testFactoryInitWithRetryProperties() { // Create factory with custom retry properties Map values = new HashMap<>(); values.put(MAX_RETRIES_PROPERTY, "5"); - values.put(RETRY_IO_EXCEPTION_PROPERTY, "false"); + values.put(RETRY_ON_ERROR_PROPERTY, "false"); // Create provider with custom properties HttpClientProvider provider = createProviderWithProperties(values); - // Get retriable HTTP client - CloseableHttpClient client = provider.getRetriableHttpClient(); + // Get HTTP client + CloseableHttpClient client = provider.getHttpClient(); // Verify client is not null - org.junit.Assert.assertNotNull("Retriable HTTP client should not be null", client); + org.junit.Assert.assertNotNull("HTTP client should not be null", client); } private Optional getTestURL() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java index 4a6efdb2c0b1..6ff7674b28d1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java @@ -23,7 +23,6 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator; import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel; import org.keycloak.common.util.PemUtils; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; From a17325a9297a76bab41f27a71a503f119cc44172 Mon Sep 17 00:00:00 2001 From: UnicornChance Date: Fri, 17 Oct 2025 07:39:01 -0600 Subject: [PATCH 6/9] Simplify HTTP client retry to single client with server-wide configuration Refactored HTTP client retry implementation based on reviewer feedback to use a single HTTP client with built-in retry behavior instead of separate retriable and non-retriable clients. Key changes: - Removed getRetriableHttpClient() method from HttpClientProvider interface - Merged retry functionality into the main httpClient in DefaultHttpClientFactory - Extracted retry configuration to configureRetries() helper method for cleaner code - Renamed retry-on-io-exception property to retry-on-error (more generic) - Removed per-feature retry configuration from X.509 authenticator - Updated OCSPProvider to use standard getHttpClient() method - Removed unused OCSP_CONNECT_TIMEOUT constant from OCSPProvider - Removed retry-specific timeout properties (using standard socket-timeout-millis) Signed-off-by: UnicornChance --- .../main/java/org/keycloak/utils/OCSPProvider.java | 1 - .../httpclient/DefaultHttpClientFactory.java | 12 +++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java b/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java index 92d79d35e2a7..577dca612758 100644 --- a/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java @@ -51,7 +51,6 @@ public abstract class OCSPProvider { private final static Logger logger = Logger.getLogger(OCSPProvider.class); - protected static final int OCSP_CONNECT_TIMEOUT = 10000; // 10 sec protected static final int TIME_SKEW = 900000; public enum RevocationStatus { diff --git a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java index 0670aafa98f0..bc99fd911e90 100755 --- a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java +++ b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java @@ -182,9 +182,13 @@ private void lazyInit(KeycloakSession session) { String clientKeystorePassword = config.get("client-keystore-password"); String clientPrivateKeyPassword = config.get("client-key-password"); boolean disableTrustManager = config.getBoolean("disable-trust-manager", false); + boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false); boolean reuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true); + // optionally configure proxy mappings + // direct SPI config (e.g. via standalone.xml) takes precedence over env vars + // lower case env vars take precedence over upper case env vars ProxyMappings proxyMappings = ProxyMappings.valueOf(config.getArray("proxy-mappings")); if (proxyMappings == null || proxyMappings.isEmpty()) { logger.debug("Trying to use proxy mapping from env vars"); @@ -193,6 +197,7 @@ private void lazyInit(KeycloakSession session) { httpProxy = getEnvVarValue(HTTP_PROXY); } String noProxy = getEnvVarValue(NO_PROXY); + logger.debugf("httpProxy: %s, noProxy: %s", httpProxy, noProxy); proxyMappings = ProxyMappings.withFixedProxyMapping(httpProxy, noProxy); } @@ -212,8 +217,9 @@ private void lazyInit(KeycloakSession session) { TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class); boolean disableTruststoreProvider = truststoreProvider == null || truststoreProvider.getTruststore() == null; + if (disableTruststoreProvider) { - logger.warn("TruststoreProvider is disabled"); + logger.warn("TruststoreProvider is disabled"); } else { builder.hostnameVerification(truststoreProvider.getPolicy()); try { @@ -224,8 +230,8 @@ private void lazyInit(KeycloakSession session) { } if (disableTrustManager) { - logger.warn("TrustManager is disabled"); - builder.disableTrustManager(); + logger.warn("TrustManager is disabled"); + builder.disableTrustManager(); } if (clientKeystore != null) { From 397a50bf5f1acb63198e412e4e0cfd7c1d22b081 Mon Sep 17 00:00:00 2001 From: UnicornChance Date: Mon, 20 Oct 2025 11:05:29 -0600 Subject: [PATCH 7/9] address feedback for more documentation and cleanup Signed-off-by: UnicornChance --- .../topics/authentication/x509.adoc | 2 +- docs/guides/server/outgoinghttp.adoc | 37 +++++++++++++++++++ .../httpclient/DefaultHttpClientFactory.java | 4 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/documentation/server_admin/topics/authentication/x509.adoc b/docs/documentation/server_admin/topics/authentication/x509.adoc index 2062da402408..9d0ef68a3993 100644 --- a/docs/documentation/server_admin/topics/authentication/x509.adoc +++ b/docs/documentation/server_admin/topics/authentication/x509.adoc @@ -143,7 +143,7 @@ Checks the certificate revocation status by using Online Certificate Status Prot *OCSP Fail-Open Behavior*:: By default the OCSP check must return a positive response in order to continue with a successful authentication. Sometimes however this check can be inconclusive: for example, the OCSP server could be unreachable, overloaded, or the client certificate may not contain an OCSP responder URI. When this setting is turned ON, authentication will be denied only if an explicit negative response is received by the OCSP responder and the certificate is definitely revoked. If a valid OCSP response is not available the authentication attempt will be accepted. -OCSP retry behavior is configured server-wide through the HTTP client provider. See the Server Configuration Guide for details on configuring retry settings for all outgoing HTTP requests, including OCSP validation. +NOTE: OCSP retry behavior is configured server-wide through the HTTP client provider. See <@links.server id="outgoinghttp"/> for details on configuring retry settings for all outgoing HTTP requests, including OCSP validation. *OCSP Responder URI*:: Override the value of the OCSP responder URI in the certificate. diff --git a/docs/guides/server/outgoinghttp.adoc b/docs/guides/server/outgoinghttp.adoc index f7642c0ce4c3..9a63e7754f0b 100644 --- a/docs/guides/server/outgoinghttp.adoc +++ b/docs/guides/server/outgoinghttp.adoc @@ -57,6 +57,43 @@ Specify proxy configurations for outgoing HTTP requests. For more details, see < *disable-trust-manager*:: If an outgoing request requires HTTPS and this configuration option is set to true, you do not have to specify a truststore. This setting should be used only during development and *never in production* because it will disable verification of SSL certificates. Default: false. +== Configuring retry behavior for outgoing HTTP requests + +{project_name} can automatically retry failed outgoing HTTP requests. This is useful for handling transient network errors or temporary service unavailability. Retry behavior is disabled by default and must be explicitly enabled. + +The following are the retry configuration options: + +*max-retries*:: +Maximum number of retry attempts for failed HTTP requests. Set to 0 to disable retries. Default: 0. + +*retry-on-error*:: +Whether to retry HTTP requests when errors occur. Default: true. + +*initial-backoff-millis*:: +Initial backoff time in milliseconds before the first retry attempt. Default: 1000. + +*backoff-multiplier*:: +Multiplier for exponential backoff between retry attempts. For example, with an initial backoff of 1000ms and a multiplier of 2.0, the retry delays would be: 1000ms, 2000ms, 4000ms, etc. Default: 2.0. + +*use-jitter*:: +Whether to apply jitter to backoff times to prevent synchronized retry storms when multiple clients are retrying at the same time. Default: true. + +*jitter-factor*:: +Jitter factor to apply to backoff times. A value of 0.5 means the actual backoff time will be between 50% and 150% of the calculated exponential backoff time. Default: 0.5. + +.Example of enabling retry behavior +[source,bash] +---- +bin/kc.[sh|bat] start --spi-connections-http-client-default-max-retries=3 \ + --spi-connections-http-client-default-retry-on-error=true \ + --spi-connections-http-client-default-initial-backoff-millis=1000 \ + --spi-connections-http-client-default-backoff-multiplier=2.0 +---- + +In this example, {project_name} will retry failed HTTP requests up to 3 times with exponential backoff starting at 1000ms and doubling with each retry attempt. + +NOTE: Retry behavior applies to all outgoing HTTP requests made by {project_name}, including OCSP validation, identity provider communication, and other external service calls. + == Proxy mappings for outgoing HTTP requests To configure outgoing requests to use a proxy, you can use the following standard proxy environment variables to configure the proxy mappings: `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`. diff --git a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java index bc99fd911e90..ff3aab1b150e 100755 --- a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java +++ b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java @@ -219,7 +219,7 @@ private void lazyInit(KeycloakSession session) { boolean disableTruststoreProvider = truststoreProvider == null || truststoreProvider.getTruststore() == null; if (disableTruststoreProvider) { - logger.warn("TruststoreProvider is disabled"); + logger.warn("TruststoreProvider is disabled"); } else { builder.hostnameVerification(truststoreProvider.getPolicy()); try { @@ -230,7 +230,7 @@ private void lazyInit(KeycloakSession session) { } if (disableTrustManager) { - logger.warn("TrustManager is disabled"); + logger.warn("TrustManager is disabled"); builder.disableTrustManager(); } From 9d1b248d312672478f851868fe5231932bf9d986 Mon Sep 17 00:00:00 2001 From: UnicornChance Date: Mon, 20 Oct 2025 11:08:17 -0600 Subject: [PATCH 8/9] fix whitespace Signed-off-by: UnicornChance --- .../connections/httpclient/DefaultHttpClientFactory.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java index ff3aab1b150e..b91e748efff5 100755 --- a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java +++ b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java @@ -219,7 +219,7 @@ private void lazyInit(KeycloakSession session) { boolean disableTruststoreProvider = truststoreProvider == null || truststoreProvider.getTruststore() == null; if (disableTruststoreProvider) { - logger.warn("TruststoreProvider is disabled"); + logger.warn("TruststoreProvider is disabled"); } else { builder.hostnameVerification(truststoreProvider.getPolicy()); try { @@ -230,8 +230,8 @@ private void lazyInit(KeycloakSession session) { } if (disableTrustManager) { - logger.warn("TrustManager is disabled"); - builder.disableTrustManager(); + logger.warn("TrustManager is disabled"); + builder.disableTrustManager(); } if (clientKeystore != null) { From e202cdf05fa8de64c6fa73b1b50a9e24c38f3ff2 Mon Sep 17 00:00:00 2001 From: UnicornChance Date: Fri, 24 Oct 2025 07:27:09 -0600 Subject: [PATCH 9/9] add doc callout about the danger of not configuring the timeout appropriately Signed-off-by: UnicornChance --- docs/guides/server/outgoinghttp.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guides/server/outgoinghttp.adoc b/docs/guides/server/outgoinghttp.adoc index 9a63e7754f0b..8088c82ba01a 100644 --- a/docs/guides/server/outgoinghttp.adoc +++ b/docs/guides/server/outgoinghttp.adoc @@ -58,6 +58,7 @@ Specify proxy configurations for outgoing HTTP requests. For more details, see < If an outgoing request requires HTTPS and this configuration option is set to true, you do not have to specify a truststore. This setting should be used only during development and *never in production* because it will disable verification of SSL certificates. Default: false. == Configuring retry behavior for outgoing HTTP requests +IMPORTANT: Do not let outgoing retry duration exceed the caller’s timeout. Otherwise, the caller may time out and see an error while {project_name} continues retrying in the background. {project_name} can automatically retry failed outgoing HTTP requests. This is useful for handling transient network errors or temporary service unavailability. Retry behavior is disabled by default and must be explicitly enabled.