Skip to content

Commit 09c2526

Browse files
committed
Add Retry option for primary OCSP service
1 parent 2712101 commit 09c2526

File tree

7 files changed

+96
-10
lines changed

7 files changed

+96
-10
lines changed

pom.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,6 @@
7171
<artifactId>resilience4j-all</artifactId>
7272
<version>${resilience4j.version}</version>
7373
<exclusions>
74-
<exclusion>
75-
<groupId>io.github.resilience4j</groupId>
76-
<artifactId>resilience4j-retry</artifactId>
77-
</exclusion>
7874
<exclusion>
7975
<groupId>io.github.resilience4j</groupId>
8076
<artifactId>resilience4j-bulkhead</artifactId>

src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import eu.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration;
2727
import eu.webeid.security.validator.ocsp.service.FallbackOcspServiceConfiguration;
2828
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
29+
import io.github.resilience4j.retry.RetryConfig;
2930
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
3031

3132
import java.net.MalformedURLException;
@@ -56,6 +57,7 @@ public final class AuthTokenValidationConfiguration {
5657
private DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration;
5758
private Collection<FallbackOcspServiceConfiguration> fallbackOcspServiceConfigurations = new HashSet<>();
5859
private CircuitBreakerConfig circuitBreakerConfig;
60+
private RetryConfig circuitBreakerRetryConfig;
5961
// Don't allow Estonian Mobile-ID policy by default.
6062
private Collection<ASN1ObjectIdentifier> disallowedSubjectCertificatePolicies = newHashSet(
6163
SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V1,
@@ -79,6 +81,7 @@ private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other)
7981
this.designatedOcspServiceConfiguration = other.designatedOcspServiceConfiguration;
8082
this.fallbackOcspServiceConfigurations = Set.copyOf(other.fallbackOcspServiceConfigurations);
8183
this.circuitBreakerConfig = other.circuitBreakerConfig;
84+
this.circuitBreakerRetryConfig = other.circuitBreakerRetryConfig;
8285
this.disallowedSubjectCertificatePolicies = Set.copyOf(other.disallowedSubjectCertificatePolicies);
8386
this.nonceDisabledOcspUrls = Set.copyOf(other.nonceDisabledOcspUrls);
8487
}
@@ -155,6 +158,14 @@ public void setCircuitBreakerConfig(CircuitBreakerConfig circuitBreakerConfig) {
155158
this.circuitBreakerConfig = circuitBreakerConfig;
156159
}
157160

161+
public RetryConfig getCircuitBreakerRetryConfig() {
162+
return circuitBreakerRetryConfig;
163+
}
164+
165+
public void setCircuitBreakerRetryConfig(RetryConfig circuitBreakerRetryConfig) {
166+
this.circuitBreakerRetryConfig = circuitBreakerRetryConfig;
167+
}
168+
158169
public boolean isRejectUnknownOcspResponseStatus() {
159170
return rejectUnknownOcspResponseStatus;
160171
}

src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import eu.webeid.security.validator.ocsp.service.FallbackOcspServiceConfiguration;
3030
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
3131
import io.github.resilience4j.core.IntervalFunction;
32+
import io.github.resilience4j.retry.RetryConfig;
3233
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
3334
import org.slf4j.Logger;
3435
import org.slf4j.LoggerFactory;
@@ -212,7 +213,6 @@ public AuthTokenValidatorBuilder withFallbackOcspServiceConfiguration(FallbackOc
212213
* @param failureRateThreshold
213214
* @param permittedNumberOfCallsInHalfOpenState
214215
* @param waitDurationInOpenState
215-
*
216216
* @return the builder instance for method chaining
217217
*/
218218
public AuthTokenValidatorBuilder withCircuitBreakerConfig(int slidingWindowSize, int minimumNumberOfCalls, int failureRateThreshold, int permittedNumberOfCallsInHalfOpenState, Duration waitDurationInOpenState) { // TODO: What do we allow to configure? Use configuration builder.
@@ -227,6 +227,17 @@ public AuthTokenValidatorBuilder withCircuitBreakerConfig(int slidingWindowSize,
227227
return this;
228228
}
229229

230+
/**
231+
* // TODO: Describe the configuration option
232+
*
233+
* @return the builder instance for method chaining
234+
*/
235+
public AuthTokenValidatorBuilder withCircuitBreakerRetryConfig() { // TODO: What do we allow to configure? Use configuration builder.
236+
configuration.setCircuitBreakerRetryConfig(RetryConfig.ofDefaults());
237+
LOG.debug("Using the OCSP circuit breaker retry configuration");
238+
return this;
239+
}
240+
230241
/**
231242
* // TODO: Describe the configuration option
232243
*

src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
9898
configuration.getFallbackOcspServiceConfigurations());
9999
resilientOcspService = new ResilientOcspService(ocspClient, ocspServiceProvider,
100100
configuration.getCircuitBreakerConfig(),
101+
configuration.getCircuitBreakerRetryConfig(),
101102
configuration.getAllowedOcspResponseTimeSkew(),
102103
configuration.getMaxOcspResponseThisUpdateAge(),
103104
configuration.isRejectUnknownOcspResponseStatus());

src/main/java/eu/webeid/security/validator/ocsp/ResilientOcspService.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
3333
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
3434
import io.github.resilience4j.decorators.Decorators;
35+
import io.github.resilience4j.retry.Retry;
36+
import io.github.resilience4j.retry.RetryConfig;
37+
import io.github.resilience4j.retry.RetryRegistry;
3538
import io.vavr.CheckedFunction0;
3639
import io.vavr.control.Try;
3740
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
@@ -61,8 +64,9 @@ public class ResilientOcspService {
6164
private final Duration maxOcspResponseThisUpdateAge;
6265
private final boolean rejectUnknownOcspResponseStatus;
6366
private final CircuitBreakerRegistry circuitBreakerRegistry;
67+
private final RetryRegistry retryRegistry;
6468

65-
public ResilientOcspService(OcspClient ocspClient, OcspServiceProvider ocspServiceProvider, CircuitBreakerConfig circuitBreakerConfig, Duration allowedOcspResponseTimeSkew, Duration maxOcspResponseThisUpdateAge, boolean rejectUnknownOcspResponseStatus) {
69+
public ResilientOcspService(OcspClient ocspClient, OcspServiceProvider ocspServiceProvider, CircuitBreakerConfig circuitBreakerConfig, RetryConfig retryConfig, Duration allowedOcspResponseTimeSkew, Duration maxOcspResponseThisUpdateAge, boolean rejectUnknownOcspResponseStatus) {
6670
this.ocspClient = ocspClient;
6771
this.ocspServiceProvider = ocspServiceProvider;
6872
this.allowedOcspResponseTimeSkew = allowedOcspResponseTimeSkew;
@@ -71,6 +75,9 @@ public ResilientOcspService(OcspClient ocspClient, OcspServiceProvider ocspServi
7175
this.circuitBreakerRegistry = CircuitBreakerRegistry.custom()
7276
.withCircuitBreakerConfig(getCircuitBreakerConfig(circuitBreakerConfig))
7377
.build();
78+
this.retryRegistry = retryConfig != null ? RetryRegistry.custom()
79+
.withRetryConfig(getRetryConfigConfig(retryConfig))
80+
.build() : null;
7481
if (LOG.isDebugEnabled()) {
7582
this.circuitBreakerRegistry.getEventPublisher()
7683
.onEntryAdded(entryAddedEvent -> {
@@ -89,10 +96,15 @@ public OcspValidationInfo validateSubjectCertificateNotRevoked(X509Certificate s
8996
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(ocspService.getAccessLocation().toASCIIString());
9097
CheckedFunction0<OcspValidationInfo> primarySupplier = () -> request(ocspService, subjectCertificate, issuerCertificate);
9198
CheckedFunction0<OcspValidationInfo> fallbackSupplier = () -> request(ocspService.getFallbackService(), subjectCertificate, issuerCertificate);
92-
CheckedFunction0<OcspValidationInfo> decoratedSupplier = Decorators.ofCheckedSupplier(primarySupplier)
93-
.withCircuitBreaker(circuitBreaker)
94-
.withFallback(List.of(UserCertificateOCSPCheckFailedException.class, CallNotPermittedException.class, UserCertificateUnknownException.class), e -> fallbackSupplier.apply())
95-
.decorate();
99+
Decorators.DecorateCheckedSupplier<OcspValidationInfo> decorateCheckedSupplier = Decorators.ofCheckedSupplier(primarySupplier);
100+
if (retryRegistry != null) {
101+
Retry retry = retryRegistry.retry(ocspService.getAccessLocation().toASCIIString());
102+
decorateCheckedSupplier.withRetry(retry);
103+
}
104+
decorateCheckedSupplier.withCircuitBreaker(circuitBreaker)
105+
.withFallback(List.of(UserCertificateOCSPCheckFailedException.class, CallNotPermittedException.class, UserCertificateUnknownException.class), e -> fallbackSupplier.apply());
106+
107+
CheckedFunction0<OcspValidationInfo> decoratedSupplier = decorateCheckedSupplier.decorate();
96108

97109
return Try.of(decoratedSupplier).getOrElseThrow(throwable -> {
98110
if (throwable instanceof AuthTokenException) {
@@ -156,6 +168,12 @@ private static CircuitBreakerConfig getCircuitBreakerConfig(CircuitBreakerConfig
156168
return configurationBuilder.build();
157169
}
158170

171+
private static RetryConfig getRetryConfigConfig(RetryConfig retryConfig) {
172+
return RetryConfig.from(retryConfig)
173+
.ignoreExceptions(UserCertificateRevokedException.class) // TODO: Revoked status is a valid response, not a failure and should be ignored. Any other exceptions to ignore?
174+
.build();
175+
}
176+
159177
CircuitBreakerRegistry getCircuitBreakerRegistry() {
160178
return circuitBreakerRegistry;
161179
}

src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedVal
353353

354354
private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidator(OcspClient client, OcspServiceProvider ocspServiceProvider) {
355355
ResilientOcspService resilientOcspService = new ResilientOcspService(client, ocspServiceProvider,
356+
null,
356357
null,
357358
CONFIGURATION.getAllowedOcspResponseTimeSkew(),
358359
CONFIGURATION.getMaxOcspResponseThisUpdateAge(),

src/test/java/eu/webeid/security/validator/ocsp/ResilientOcspServiceTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
3333
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
3434
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
35+
import io.github.resilience4j.retry.RetryConfig;
3536
import org.bouncycastle.cert.ocsp.OCSPResp;
3637
import org.bouncycastle.jce.provider.BouncyCastleProvider;
3738
import org.junit.jupiter.api.BeforeAll;
@@ -121,6 +122,7 @@ void whenFallbackConfigured_thenFallbackAndRecoverySucceeds() throws Exception {
121122
ocspClient,
122123
ocspServiceProvider,
123124
circuitBreakerConfig,
125+
null,
124126
ALLOWED_TIME_SKEW,
125127
MAX_THIS_UPDATE_AGE,
126128
false
@@ -206,6 +208,7 @@ void whenOcspResponseGood_thenNoFallbackAndSucceeds() throws Exception {
206208
ocspClient,
207209
ocspServiceProvider,
208210
null,
211+
null,
209212
ALLOWED_TIME_SKEW,
210213
MAX_THIS_UPDATE_AGE,
211214
false
@@ -230,6 +233,45 @@ void whenOcspResponseGood_thenNoFallbackAndSucceeds() throws Exception {
230233
}
231234
}
232235

236+
@Test
237+
void whenRetryEnabledAndRetrySucceeds_thenNoFallbackAndSucceeds() throws Exception {
238+
final OcspClient ocspClient = mock(OcspClient.class);
239+
when(ocspClient.request(eq(PRIMARY_OCSP_URL), any()))
240+
.thenThrow(new IOException("Mocked exception 1"))
241+
.thenThrow(new IOException("Mocked exception 2"))
242+
.thenReturn(new OCSPResp(validOcspResponseBytes));
243+
when(ocspClient.request(eq(FALLBACK_OCSP_URL), any()))
244+
.thenReturn(new OCSPResp(validOcspResponseBytes));
245+
OcspServiceProvider ocspServiceProvider = createOcspServiceProviderWithFallback();
246+
ResilientOcspService resilientOcspService = new ResilientOcspService(
247+
ocspClient,
248+
ocspServiceProvider,
249+
null,
250+
RetryConfig.ofDefaults(), // Retry enabled
251+
ALLOWED_TIME_SKEW,
252+
MAX_THIS_UPDATE_AGE,
253+
false
254+
);
255+
CircuitBreakerRegistry circuitBreakerRegistry = resilientOcspService.getCircuitBreakerRegistry();
256+
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(PRIMARY_OCSP_URL.toASCIIString());
257+
try (var mockedClock = mockStatic(DateAndTime.DefaultClock.class)) {
258+
mockDate("2021-09-17T18:25:24", mockedClock);
259+
260+
OcspValidationInfo validationInfo = resilientOcspService.validateSubjectCertificateNotRevoked(subjectCertificate, issuerCertificate);
261+
assertThat(validationInfo).isNotNull();
262+
assertThat(validationInfo).extracting(OcspValidationInfo::getSubjectCertificate)
263+
.isEqualTo(subjectCertificate);
264+
assertThat(validationInfo).extracting(OcspValidationInfo::getOcspResponderUri)
265+
.isEqualTo(new URI("http://aia.demo.sk.ee/esteid2018"));
266+
assertThat(validationInfo).extracting(OcspValidationInfo::getOcspResponse)
267+
.isNotNull();
268+
269+
verify(ocspClient, times(3)).request(eq(PRIMARY_OCSP_URL), any());
270+
verify(ocspClient, times(0)).request(eq(FALLBACK_OCSP_URL), any());
271+
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
272+
}
273+
}
274+
233275
@Test
234276
void whenOcspResponseRevoked_thenNoFallbackAndThrows() throws Exception {
235277
final OcspClient ocspClient = mock(OcspClient.class);
@@ -242,6 +284,7 @@ void whenOcspResponseRevoked_thenNoFallbackAndThrows() throws Exception {
242284
ocspClient,
243285
ocspServiceProvider,
244286
null,
287+
null,
245288
ALLOWED_TIME_SKEW,
246289
MAX_THIS_UPDATE_AGE,
247290
false
@@ -279,6 +322,7 @@ void whenOcspResponseUnknown_thenNoFallbackAndThrows() throws Exception {
279322
ocspClient,
280323
ocspServiceProvider,
281324
null,
325+
null,
282326
ALLOWED_TIME_SKEW,
283327
MAX_THIS_UPDATE_AGE,
284328
false
@@ -316,6 +360,7 @@ void whenPrimaryOcspResponseUnknownAndRejectUnknownOcspResponseStatusConfigurati
316360
ocspClient,
317361
ocspServiceProvider,
318362
null,
363+
null,
319364
ALLOWED_TIME_SKEW,
320365
MAX_THIS_UPDATE_AGE,
321366
true // rejectUnknownOcspResponseStatus
@@ -358,6 +403,7 @@ void whenPrimaryAndFallbackRevocationStatusUnknownAndRejectUnknownOcspResponseSt
358403
ocspClient,
359404
ocspServiceProvider,
360405
null,
406+
null,
361407
ALLOWED_TIME_SKEW,
362408
MAX_THIS_UPDATE_AGE,
363409
true // rejectUnknownOcspResponseStatus
@@ -396,6 +442,7 @@ void whenPrimaryAndFallbackConnectionFail_thenThrows() throws Exception {
396442
ocspClient,
397443
ocspServiceProvider,
398444
null,
445+
null,
399446
ALLOWED_TIME_SKEW,
400447
MAX_THIS_UPDATE_AGE,
401448
false
@@ -430,6 +477,7 @@ void whenNoFallbackConfigured_thenPrimaryFailureThrows() throws Exception {
430477
ocspClient,
431478
ocspServiceProvider,
432479
null,
480+
null,
433481
ALLOWED_TIME_SKEW,
434482
MAX_THIS_UPDATE_AGE,
435483
false

0 commit comments

Comments
 (0)