Skip to content

Commit 73dbaa2

Browse files
authored
Merge pull request quarkusio#47830 from sberyozkin/oidc_healthcheck
Add OIDC Health Check
2 parents 2289965 + 5cf69c4 commit 73dbaa2

File tree

19 files changed

+293
-2
lines changed

19 files changed

+293
-2
lines changed

docs/src/main/asciidoc/security-oidc-expanded-configuration.adoc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,20 @@ You can observe an https://quarkus.io/guides/security-openid-connect-multitenanc
10671067

10681068
You can register xref:security-openid-connect-multitenancy.adoc#tenant-config-resolver[TenantConfigResolver] and build the configuration dynamically, using `OicTenantConfig` builder API, using the request properties such as request path and headers for additional hints or retrieve the matching configuration from the external sources.
10691069

1070+
.OIDC Health Check
1071+
[options="header"]
1072+
|====
1073+
|Property name |Default |Description
1074+
1075+
|quarkus.oidc.health.enabled |false|If the OIDC Health Readiness Check must be registered.
1076+
|====
1077+
1078+
This build-time property can be used to register an `OIDC Provider Health Readiness Check` when the `quarkus-smallrye-health` dependency is included. When the health check is registered, it uses HTTP HEAD to ping the well-known OIDC provider configuration endpoint for every configured OIDC tenant.
1079+
1080+
Individual OIDC tenant statuses are `OK`, `Not Ready`, `Disabled`, `Unknown` and `Error`.
1081+
1082+
The OIDC Health check status is `UP` if at least one of the OIDC tenants has an `OK` status and `DOWN` otherwise.
1083+
10701084
[[typical-property-combinations]]
10711085
== Typical property combinations
10721086

extensions/oidc/deployment/pom.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@
4545
<groupId>io.quarkus</groupId>
4646
<artifactId>quarkus-jsonp-deployment</artifactId>
4747
</dependency>
48+
<dependency>
49+
<groupId>io.quarkus</groupId>
50+
<artifactId>quarkus-smallrye-health-spi</artifactId>
51+
</dependency>
52+
<dependency>
53+
<groupId>io.quarkus</groupId>
54+
<artifactId>quarkus-smallrye-health-deployment</artifactId>
55+
<optional>true</optional>
56+
</dependency>
4857
<dependency>
4958
<groupId>org.eclipse.angus</groupId>
5059
<artifactId>angus-activation</artifactId>

extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,15 @@
9999
import io.quarkus.oidc.runtime.OidcTokenCredentialProducer;
100100
import io.quarkus.oidc.runtime.OidcUtils;
101101
import io.quarkus.oidc.runtime.TenantConfigBean;
102+
import io.quarkus.oidc.runtime.health.OidcTenantHealthCheck;
102103
import io.quarkus.oidc.runtime.providers.AzureAccessTokenCustomizer;
103104
import io.quarkus.runtime.configuration.ConfigurationException;
104105
import io.quarkus.security.Authenticated;
105106
import io.quarkus.security.runtime.SecurityConfig;
106107
import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem;
107108
import io.quarkus.security.spi.ClassSecurityAnnotationBuildItem;
108109
import io.quarkus.security.spi.RegisterClassSecurityCheckBuildItem;
110+
import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem;
109111
import io.quarkus.tls.deployment.spi.TlsRegistryBuildItem;
110112
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
111113
import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorBindingBuildItem;
@@ -472,6 +474,14 @@ public void registerAuthenticationContextInterceptor(Capabilities capabilities,
472474
.builder(Authenticated.class).buildWithTarget(c))));
473475
}
474476

477+
@BuildStep
478+
public void registerHealthCheck(OidcBuildTimeConfig config, BuildProducer<HealthBuildItem> healthBuildItems,
479+
Capabilities capabilities) {
480+
if (config.healthEnabled() && capabilities.isPresent(Capability.SMALLRYE_HEALTH)) {
481+
healthBuildItems.produce(new HealthBuildItem(OidcTenantHealthCheck.class.getName(), true));
482+
}
483+
}
484+
475485
private static boolean areEagerSecInterceptorsSupported(Capabilities capabilities,
476486
VertxHttpBuildTimeConfig httpBuildTimeConfig) {
477487
if (httpBuildTimeConfig.auth().proactive()) {

extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.quarkus.runtime.annotations.ConfigRoot;
66
import io.smallrye.config.ConfigMapping;
77
import io.smallrye.config.WithDefault;
8+
import io.smallrye.config.WithName;
89

910
/**
1011
* Build time configuration for OIDC.
@@ -31,4 +32,12 @@ public interface OidcBuildTimeConfig {
3132
*/
3233
@WithDefault("true")
3334
boolean defaultTokenCacheEnabled();
35+
36+
/**
37+
* Whether the OIDC extension should automatically register a health check for OIDC tenants
38+
* when a Health Check capability is present.
39+
*/
40+
@WithName("health.enabled")
41+
@WithDefault("false")
42+
boolean healthEnabled();
3443
}

extensions/oidc/runtime/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
<groupId>jakarta.annotation</groupId>
4646
<artifactId>jakarta.annotation-api</artifactId>
4747
</dependency>
48+
<dependency>
49+
<groupId>io.quarkus</groupId>
50+
<artifactId>quarkus-smallrye-health</artifactId>
51+
<optional>true</optional>
52+
</dependency>
4853
<dependency>
4954
<groupId>io.quarkus</groupId>
5055
<artifactId>quarkus-junit5-internal</artifactId>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package io.quarkus.oidc.runtime.health;
2+
3+
import java.util.Map.Entry;
4+
5+
import jakarta.inject.Inject;
6+
7+
import org.eclipse.microprofile.health.HealthCheck;
8+
import org.eclipse.microprofile.health.HealthCheckResponse;
9+
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
10+
import org.eclipse.microprofile.health.Readiness;
11+
12+
import io.quarkus.oidc.runtime.OidcProviderClientImpl;
13+
import io.quarkus.oidc.runtime.OidcUtils;
14+
import io.quarkus.oidc.runtime.TenantConfigBean;
15+
import io.quarkus.oidc.runtime.TenantConfigContext;
16+
import io.smallrye.mutiny.Uni;
17+
import io.vertx.mutiny.core.buffer.Buffer;
18+
import io.vertx.mutiny.ext.web.client.HttpRequest;
19+
20+
@Readiness
21+
public class OidcTenantHealthCheck implements HealthCheck {
22+
private static final String HEALTH_CHECK_NAME = "OIDC Provider Health Check";
23+
24+
private static final String OK_STATUS = "OK";
25+
private static final String ERROR_STATUS = "Error";
26+
private static final String DISABLED_STATUS = "Disabled";
27+
private static final String UNKNOWN_STATUS = "Unknown";
28+
private static final String NOT_READY_STATUS = "Not Ready";
29+
30+
@Inject
31+
TenantConfigBean tenantConfigBean;
32+
33+
@Override
34+
public HealthCheckResponse call() {
35+
HealthCheckResponseBuilder builder = HealthCheckResponse.builder()
36+
.name(HEALTH_CHECK_NAME)
37+
.up();
38+
39+
String status = checkTenant(builder, OidcUtils.DEFAULT_TENANT_ID, tenantConfigBean.getDefaultTenant());
40+
boolean atLeastOneTenantIsReady = OK_STATUS.equals(status);
41+
42+
for (Entry<String, TenantConfigContext> entry : tenantConfigBean.getStaticTenantsConfig().entrySet()) {
43+
status = checkTenant(builder, entry.getKey(), entry.getValue());
44+
if (!atLeastOneTenantIsReady) {
45+
atLeastOneTenantIsReady = OK_STATUS.equals(status);
46+
}
47+
}
48+
49+
if (!atLeastOneTenantIsReady) {
50+
builder.down();
51+
}
52+
return builder.build();
53+
}
54+
55+
private static String checkTenant(HealthCheckResponseBuilder builder, String tenantId,
56+
TenantConfigContext tenantConfigContext) {
57+
58+
if (tenantConfigContext.oidcConfig() == null) {
59+
return null;
60+
}
61+
String name = tenantConfigContext.oidcConfig().clientName().orElse(tenantId);
62+
63+
String status = null;
64+
if (tenantConfigContext.getOidcProviderClient() == null) {
65+
if (!tenantConfigContext.oidcConfig().tenantEnabled()) {
66+
status = DISABLED_STATUS;
67+
} else if (!tenantConfigContext.ready()) {
68+
status = NOT_READY_STATUS;
69+
}
70+
} else if (tenantConfigContext.getOidcMetadata().getDiscoveryUri() == null) {
71+
// We may introduce a metadata health property
72+
status = UNKNOWN_STATUS;
73+
} else {
74+
try {
75+
status = checkHealth(tenantConfigContext.getOidcProviderClient(),
76+
tenantConfigContext.getOidcMetadata().getDiscoveryUri()).await().indefinitely();
77+
} catch (Exception e) {
78+
status = ERROR_STATUS + ": " + e.getMessage();
79+
}
80+
}
81+
if (status != null) {
82+
builder.withData(name, status);
83+
}
84+
return status;
85+
}
86+
87+
private static Uni<String> checkHealth(OidcProviderClientImpl oidcClient, String healthUri) {
88+
89+
HttpRequest<Buffer> request = oidcClient.getWebClient().headAbs(healthUri);
90+
return request.send().onItem().transform(resp -> {
91+
Buffer buffer = resp.body();
92+
if (resp.statusCode() == 200) {
93+
return OK_STATUS;
94+
} else {
95+
String errorMessage = buffer != null ? buffer.toString() : null;
96+
if (errorMessage != null && !errorMessage.isEmpty()) {
97+
return ERROR_STATUS + ": " + errorMessage;
98+
} else {
99+
return ERROR_STATUS;
100+
}
101+
102+
}
103+
});
104+
105+
}
106+
}

integration-tests/oidc-code-flow/pom.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
<groupId>io.quarkus</groupId>
2323
<artifactId>quarkus-smallrye-jwt-build</artifactId>
2424
</dependency>
25+
<dependency>
26+
<groupId>io.quarkus</groupId>
27+
<artifactId>quarkus-smallrye-health</artifactId>
28+
</dependency>
2529
<dependency>
2630
<groupId>io.quarkus</groupId>
2731
<artifactId>quarkus-resteasy-jackson</artifactId>
@@ -116,6 +120,19 @@
116120
</exclusion>
117121
</exclusions>
118122
</dependency>
123+
<dependency>
124+
<groupId>io.quarkus</groupId>
125+
<artifactId>quarkus-smallrye-health-deployment</artifactId>
126+
<version>${project.version}</version>
127+
<type>pom</type>
128+
<scope>test</scope>
129+
<exclusions>
130+
<exclusion>
131+
<groupId>*</groupId>
132+
<artifactId>*</artifactId>
133+
</exclusion>
134+
</exclusions>
135+
</dependency>
119136
<dependency>
120137
<groupId>io.quarkus</groupId>
121138
<artifactId>quarkus-resteasy-jackson-deployment</artifactId>

integration-tests/oidc-code-flow/src/main/resources/application.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
quarkus.oidc.health.enabled=true
2+
13
quarkus.keycloak.devservices.create-realm=false
24
quarkus.keycloak.devservices.show-logs=true
35
# Default tenant configuration
46
quarkus.oidc.client-id=quarkus-app
7+
quarkus.oidc.client-name=Quarkus Keycloak
58
quarkus.oidc.credentials.secret=secret
69
quarkus.oidc.authentication.scopes=profile,email
710
quarkus.oidc.authentication.redirect-path=/web-app

integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import io.quarkus.test.junit.QuarkusTest;
4343
import io.quarkus.test.keycloak.client.KeycloakTestClient;
4444
import io.restassured.RestAssured;
45+
import io.restassured.response.Response;
4546
import io.smallrye.jwt.build.Jwt;
4647
import io.smallrye.jwt.util.KeyUtils;
4748
import io.vertx.core.json.JsonObject;
@@ -140,9 +141,24 @@ public void testCodeFlowNoConsent() throws IOException {
140141
.then().statusCode(401);
141142

142143
webClient.getCookieManager().clearCookies();
144+
145+
checkHealth();
143146
}
144147
}
145148

149+
private static void checkHealth() {
150+
Response healthReadyResponse = RestAssured.when().get("http://localhost:8081/q/health/ready");
151+
JsonObject jsonHealth = new JsonObject(healthReadyResponse.asString());
152+
JsonObject oidcCheck = jsonHealth.getJsonArray("checks").getJsonObject(0);
153+
assertEquals("UP", oidcCheck.getString("status"));
154+
assertEquals("OIDC Provider Health Check", oidcCheck.getString("name"));
155+
156+
JsonObject data = oidcCheck.getJsonObject("data");
157+
assertEquals("OK", data.getString("tenant-nonce"));
158+
assertEquals("OK", data.getString("Quarkus Keycloak"));
159+
160+
}
161+
146162
@Test
147163
public void testCodeFlowScopeError() throws IOException {
148164
try (final WebClient webClient = createWebClient()) {

integration-tests/oidc-dpop/pom.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@
3232
</exclusion>
3333
</exclusions>
3434
</dependency>
35+
<dependency>
36+
<groupId>io.quarkus</groupId>
37+
<artifactId>quarkus-smallrye-health</artifactId>
38+
</dependency>
39+
<dependency>
40+
<groupId>io.quarkus</groupId>
41+
<artifactId>quarkus-smallrye-health-deployment</artifactId>
42+
<version>${project.version}</version>
43+
<type>pom</type>
44+
<scope>test</scope>
45+
<exclusions>
46+
<exclusion>
47+
<groupId>*</groupId>
48+
<artifactId>*</artifactId>
49+
</exclusion>
50+
</exclusions>
51+
</dependency>
3552
<dependency>
3653
<groupId>io.quarkus</groupId>
3754
<artifactId>quarkus-rest</artifactId>

0 commit comments

Comments
 (0)