Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/client-jersey.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public class OidcConfiguration {

private String scope;

private boolean enableStartupValidation;

private CacheConfiguration cache = new CacheConfiguration();

/**
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

Expand All @@ -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.
*
* <p>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.
*
* <p>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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Loading