diff --git a/docs/client-jersey.md b/docs/client-jersey.md index 1253c2e367..53056a4c55 100644 --- a/docs/client-jersey.md +++ b/docs/client-jersey.md @@ -256,6 +256,7 @@ Example config for **production** to be used with environment variables of the c ```yaml oidc: disabled: ${OIDC_DISABLED:-false} + enableStartupValidation: ${OIDC_ENABLE_STARTUP_VALIDATION:-false} grantType: ${OIDC_GRANT_TYPE:-client_credentials} clientId: ${OIDC_CLIENT_ID} clientSecret: ${OIDC_CLIENT_SECRET} @@ -270,6 +271,13 @@ oidc: * Disable retrieving a new token completely. Not meant for production! * Example: `false` +- _OIDC_ENABLE_STARTUP_VALIDATION_ + * Enables startup validation of the OIDC configuration by performing an initial access-token retrieval. + Set to `true` to fail fast during startup if the OIDC provider is unreachable or misconfigured. + * Note: this only verifies that a token can be retrieved — it does **not** check whether the token + has the required scopes or permissions to access the target resource server. + * Default: `false` + - _OIDC_GRANT_TYPE_ * Sets the OIDC grant type. * Default: `client_credentials` diff --git a/sda-commons-client-jersey/src/main/java/org/sdase/commons/client/jersey/oidc/OidcConfiguration.java b/sda-commons-client-jersey/src/main/java/org/sdase/commons/client/jersey/oidc/OidcConfiguration.java index cf28ff3652..2a73b37098 100644 --- a/sda-commons-client-jersey/src/main/java/org/sdase/commons/client/jersey/oidc/OidcConfiguration.java +++ b/sda-commons-client-jersey/src/main/java/org/sdase/commons/client/jersey/oidc/OidcConfiguration.java @@ -48,6 +48,8 @@ public class OidcConfiguration { private String scope; + private boolean enableStartupValidation; + private CacheConfiguration cache = new CacheConfiguration(); /** @@ -130,6 +132,15 @@ public OidcConfiguration setScope(String scope) { return this; } + public boolean isEnableStartupValidation() { + return enableStartupValidation; + } + + public OidcConfiguration setEnableStartupValidation(boolean enableStartupValidation) { + this.enableStartupValidation = enableStartupValidation; + return this; + } + public String getIssuerUrl() { return issuerUrl; } diff --git a/sda-commons-client-jersey/src/main/java/org/sdase/commons/client/jersey/oidc/filter/OidcRequestFilter.java b/sda-commons-client-jersey/src/main/java/org/sdase/commons/client/jersey/oidc/filter/OidcRequestFilter.java index be084061dd..bd192ea17d 100644 --- a/sda-commons-client-jersey/src/main/java/org/sdase/commons/client/jersey/oidc/filter/OidcRequestFilter.java +++ b/sda-commons-client-jersey/src/main/java/org/sdase/commons/client/jersey/oidc/filter/OidcRequestFilter.java @@ -22,17 +22,21 @@ public class OidcRequestFilter implements ClientRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(OidcRequestFilter.class); + private final OidcConfiguration oidcConfiguration; + private OidcClient oidcClient; private AuthHeaderClientFilter authHeaderClientFilter; public OidcRequestFilter( ClientFactory clientFactory, OidcConfiguration oidc, boolean authenticationPassthrough) { + this.oidcConfiguration = oidc; if (authenticationPassthrough) { authHeaderClientFilter = new AuthHeaderClientFilter(); } if (!oidc.isDisabled()) { this.oidcClient = new OidcClient(clientFactory, oidc); + this.validateOidcConfig(); } } @@ -41,12 +45,55 @@ public OidcRequestFilter( OidcConfiguration oidc, boolean authenticationPassthrough, String clientName) { + this.oidcConfiguration = oidc; if (authenticationPassthrough) { authHeaderClientFilter = new AuthHeaderClientFilter(); } if (!oidc.isDisabled()) { this.oidcClient = new OidcClient(clientFactory, oidc, clientName); + this.validateOidcConfig(); + } + } + + /** + * Validates that an OIDC access token can be retrieved successfully. + * + *

This method is called during construction when OIDC is enabled. It enforces fast startup + * failure for invalid OIDC settings, unreachable token endpoints, or credentials that cannot + * retrieve a token. + * + *

Validation is skipped when {@link OidcConfiguration#isEnableStartupValidation()} returns + * {@code false}. The constructors also skip this validation when OIDC is disabled. + * + * @throws IllegalStateException if startup validation is enabled and the OIDC client is not + * initialized, the token endpoint cannot be reached, or no valid access token can be + * retrieved + */ + private void validateOidcConfig() { + if (!oidcConfiguration.isEnableStartupValidation()) { + return; } + + OidcResult oidcResult; + try { + oidcResult = oidcClient.createAccessToken(); + } catch (RuntimeException e) { + throw new IllegalStateException( + buildOidcConfigValidationErrorMessage("Token retrieval failed."), e); + } + + if (oidcResult == null || oidcResult.getState() == OidcState.ERROR) { + throw new IllegalStateException( + buildOidcConfigValidationErrorMessage( + "Could not retrieve access token. State was: " + + (oidcResult == null ? "null" : oidcResult.getState()))); + } + } + + private String buildOidcConfigValidationErrorMessage(String detail) { + return String.format( + "OIDC startup validation failed. %s Configuration: issuerUrl='%s', grantType='%s'. Check OIDC provider settings.", + detail, oidcConfiguration.getIssuerUrl(), oidcConfiguration.getGrantType()); } @Override diff --git a/sda-commons-client-jersey/src/test/java/org/sdase/commons/client/jersey/oidc/OidcRequestFilterTest.java b/sda-commons-client-jersey/src/test/java/org/sdase/commons/client/jersey/oidc/OidcRequestFilterTest.java index 325dccf315..493da04775 100644 --- a/sda-commons-client-jersey/src/test/java/org/sdase/commons/client/jersey/oidc/OidcRequestFilterTest.java +++ b/sda-commons-client-jersey/src/test/java/org/sdase/commons/client/jersey/oidc/OidcRequestFilterTest.java @@ -10,6 +10,8 @@ import static org.apache.hc.core5.http.HttpHeaders.ACCEPT; import static org.apache.hc.core5.http.HttpHeaders.CONTENT_TYPE; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.codahale.metrics.MetricFilter; import com.github.tomakehurst.wiremock.client.BasicCredentials; @@ -162,6 +164,50 @@ void shouldUseCustomNameForMetrics() { assertThat(metrics).anyMatch(m -> m.contains(".custom-oidc-client.")); } + @Test + void shouldThrowOnConstructionWhenStartupValidationEnabledAndTokenEndpointFails() { + // given – token endpoint returns no body → OidcState.ERROR + setupTokenEndpoint(false); + var clientFactory = app.getJerseyClientBundle().getClientFactory(); + var config = buildOidcConfig(true); + + // when / then + assertThatThrownBy(() -> new OidcRequestFilter(clientFactory, config, true)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("OIDC startup validation failed"); + } + + @Test + void shouldNotThrowOnConstructionWhenStartupValidationEnabledAndTokenEndpointWorks() { + // given – token endpoint returns a valid token + setupTokenEndpoint(true); + var clientFactory = app.getJerseyClientBundle().getClientFactory(); + var config = buildOidcConfig(true); + + // when / then – no exception + assertThatNoException().isThrownBy(() -> new OidcRequestFilter(clientFactory, config, true)); + } + + @Test + void shouldNotThrowOnConstructionWhenStartupValidationIsDisabled() { + // given – token endpoint returns no body → OidcState.ERROR, but validation is disabled + setupTokenEndpoint(false); + var clientFactory = app.getJerseyClientBundle().getClientFactory(); + var config = buildOidcConfig(false); + + // when / then – no exception despite broken token endpoint + assertThatNoException().isThrownBy(() -> new OidcRequestFilter(clientFactory, config, true)); + } + + private OidcConfiguration buildOidcConfig(boolean enableStartupValidation) { + return new OidcConfiguration() + .setIssuerUrl(WIRE.baseUrl() + "/issuer") + .setClientId(CLIENT_ID) + .setClientSecret(CLIENT_SECRET) + .setUseAuthHeader(true) + .setEnableStartupValidation(enableStartupValidation); + } + private void initializeAuthenticationContext() { Mockito.when(container.getHeaders()) .thenReturn(