Skip to content

Commit

Permalink
Merge pull request #37760 from michalvavrik/feature/oidc-event-for-en…
Browse files Browse the repository at this point in the history
…dpoint-not-available

Fire OIDC security event when OIDC server not available
  • Loading branch information
sberyozkin committed Dec 17, 2023
2 parents 350e37f + 8811efc commit cdbb8e1
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 17 deletions.
2 changes: 1 addition & 1 deletion docs/src/main/asciidoc/security-customization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ public class SecurityEventObserver {
void observeAuthorizationSuccess(@ObservesAsync AuthorizationSuccessEvent event) {
String principalName = getPrincipalName(event);
if (principalName != null) {
LOG.debugf("User '%s' has been authorized successfully", event.getSecurityIdentity().getPrincipal().getName());
LOG.debugf("User '%s' has been authorized successfully", principalName);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@
*/
public class SecurityEvent extends AbstractSecurityEvent {
public static final String SESSION_TOKENS_PROPERTY = "session-tokens";
public static final String AUTH_SERVER_URL = "auth-server-url";

public enum Type {
/**
* OIDC connection event which is reported when an attempt to connect to the OIDC server has failed.
*/
OIDC_SERVER_NOT_AVAILABLE,
/**
* OIDC connection event which is reported when a connection to the OIDC server has been recovered.
*/
OIDC_SERVER_AVAILABLE,
/**
* OIDC Login event which is reported after the first user authentication but also when the user's session
* has expired and the user has re-authenticated at the OIDC provider site.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.quarkus.oidc.runtime;

import static io.quarkus.oidc.SecurityEvent.AUTH_SERVER_URL;
import static io.quarkus.oidc.SecurityEvent.Type.OIDC_SERVER_AVAILABLE;
import static io.quarkus.oidc.SecurityEvent.Type.OIDC_SERVER_NOT_AVAILABLE;
import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID;
import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getRoutingContextAttribute;

Expand Down Expand Up @@ -28,6 +31,7 @@
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.OidcTenantConfig.Roles.Source;
import io.quarkus.oidc.OidcTenantConfig.TokenStateManager.Strategy;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.oidc.common.OidcEndpoint;
Expand All @@ -42,8 +46,10 @@
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.security.runtime.SecurityConfig;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.security.spi.runtime.MethodDescription;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.jwt.util.KeyUtils;
import io.smallrye.mutiny.Uni;
Expand All @@ -60,6 +66,7 @@ public class OidcRecorder {
private static final Logger LOG = Logger.getLogger(OidcRecorder.class);

private static final Map<String, TenantConfigContext> dynamicTenantsConfig = new ConcurrentHashMap<>();
private static final Set<String> tenantsExpectingServerAvailableEvents = ConcurrentHashMap.newKeySet();

public Supplier<DefaultTokenIntrospectionUserInfoCache> setupTokenCache(OidcConfig config, Supplier<Vertx> vertx) {
return new Supplier<DefaultTokenIntrospectionUserInfoCache>() {
Expand Down Expand Up @@ -356,9 +363,10 @@ public static Optional<ProxyOptions> toProxyOptions(OidcCommonConfig.Proxy proxy
return OidcCommonUtils.toProxyOptions(proxyConfig);
}

protected static OIDCException toOidcException(Throwable cause, String authServerUrl) {
protected static OIDCException toOidcException(Throwable cause, String authServerUrl, String tenantId) {
final String message = OidcCommonUtils.formatConnectionErrorMessage(authServerUrl);
LOG.warn(message);
fireOidcServerNotAvailableEvent(authServerUrl, tenantId);
return new OIDCException("OIDC Server is not available", cause);
}

Expand Down Expand Up @@ -418,25 +426,41 @@ private static Key readTokenDecryptionKey(OidcTenantConfig oidcConfig) {

protected static Uni<JsonWebKeySet> getJsonWebSetUni(OidcProviderClient client, OidcTenantConfig oidcConfig) {
if (!oidcConfig.isDiscoveryEnabled().orElse(true)) {
final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
return client.getJsonWebKeySet(null).onFailure(OidcCommonUtils.oidcEndpointNotAvailable())
.retry()
.withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION, OidcCommonUtils.CONNECTION_BACKOFF_DURATION)
.expireIn(connectionDelayInMillisecs)
.onFailure()
.transform(new Function<Throwable, Throwable>() {
@Override
public Throwable apply(Throwable t) {
return toOidcException(t, oidcConfig.authServerUrl.get());
}
})
.onFailure()
.invoke(client::close);
String tenantId = oidcConfig.tenantId.orElse(DEFAULT_TENANT_ID);
if (shouldFireOidcServerAvailableEvent(tenantId)) {
return getJsonWebSetUniWhenDiscoveryDisabled(client, oidcConfig)
.invoke(new Runnable() {
@Override
public void run() {
fireOidcServerAvailableEvent(oidcConfig.authServerUrl.get(), tenantId);
}
});
}
return getJsonWebSetUniWhenDiscoveryDisabled(client, oidcConfig);
} else {
return client.getJsonWebKeySet(null);
}
}

private static Uni<JsonWebKeySet> getJsonWebSetUniWhenDiscoveryDisabled(OidcProviderClient client,
OidcTenantConfig oidcConfig) {
final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
return client.getJsonWebKeySet(null).onFailure(OidcCommonUtils.oidcEndpointNotAvailable())
.retry()
.withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION, OidcCommonUtils.CONNECTION_BACKOFF_DURATION)
.expireIn(connectionDelayInMillisecs)
.onFailure()
.transform(new Function<Throwable, Throwable>() {
@Override
public Throwable apply(Throwable t) {
return toOidcException(t, oidcConfig.authServerUrl.get(),
oidcConfig.tenantId.orElse(DEFAULT_TENANT_ID));
}
})
.onFailure()
.invoke(client::close);
}

protected static Uni<OidcProviderClient> createOidcClientUni(OidcTenantConfig oidcConfig,
TlsConfig tlsConfig, Vertx vertx) {

Expand Down Expand Up @@ -471,9 +495,13 @@ public OidcConfigurationMetadata apply(JsonObject json) {

@Override
public Uni<OidcProviderClient> apply(OidcConfigurationMetadata metadata, Throwable t) {
String tenantId = oidcConfig.tenantId.orElse(DEFAULT_TENANT_ID);
if (t != null) {
client.close();
return Uni.createFrom().failure(toOidcException(t, authServerUriString));
return Uni.createFrom().failure(toOidcException(t, authServerUriString, tenantId));
}
if (shouldFireOidcServerAvailableEvent(tenantId)) {
fireOidcServerAvailableEvent(authServerUriString, tenantId);
}
if (metadata == null) {
client.close();
Expand Down Expand Up @@ -514,6 +542,32 @@ private static OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oi
oidcConfig.token.issuer.orElse(null));
}

private static void fireOidcServerNotAvailableEvent(String authServerUrl, String tenantId) {
if (fireOidcServerEvent(authServerUrl, OIDC_SERVER_NOT_AVAILABLE)) {
tenantsExpectingServerAvailableEvents.add(tenantId);
}
}

private static void fireOidcServerAvailableEvent(String authServerUrl, String tenantId) {
if (fireOidcServerEvent(authServerUrl, OIDC_SERVER_AVAILABLE)) {
tenantsExpectingServerAvailableEvents.remove(tenantId);
}
}

private static boolean shouldFireOidcServerAvailableEvent(String tenantId) {
return tenantsExpectingServerAvailableEvents.contains(tenantId);
}

private static boolean fireOidcServerEvent(String authServerUrl, SecurityEvent.Type eventType) {
if (Arc.container().instance(SecurityConfig.class).get().events().enabled()) {
SecurityEventHelper.fire(
Arc.container().beanManager().getEvent().select(SecurityEvent.class),
new SecurityEvent(eventType, Map.of(AUTH_SERVER_URL, authServerUrl)));
return true;
}
return false;
}

public Consumer<RoutingContext> createTenantResolverInterceptor(String tenantId) {
return new Consumer<RoutingContext>() {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkus.it.keycloak;

import static io.quarkus.oidc.SecurityEvent.AUTH_SERVER_URL;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import jakarta.enterprise.event.Observes;
import jakarta.inject.Singleton;

import io.quarkus.oidc.SecurityEvent;

@Singleton
public class OidcEventObserver {

private final Set<String> unavailableAuthServerUrls = ConcurrentHashMap.newKeySet();
private final Set<String> availableAuthServerUrls = ConcurrentHashMap.newKeySet();

void observe(@Observes SecurityEvent event) {
if (event.getEventType() == SecurityEvent.Type.OIDC_SERVER_NOT_AVAILABLE) {
String authServerUrl = dropTrailingSlash((String) event.getEventProperties().get(AUTH_SERVER_URL));
unavailableAuthServerUrls.add(authServerUrl);
} else if (event.getEventType() == SecurityEvent.Type.OIDC_SERVER_AVAILABLE) {
String authServerUrl = (String) event.getEventProperties().get(AUTH_SERVER_URL);
availableAuthServerUrls.add(authServerUrl);
}
}

public static String dropTrailingSlash(String authServerUrl) {
if (authServerUrl.endsWith("/")) {
// drop ending slash as these auth server urls are same in principle
return authServerUrl.substring(0, authServerUrl.length() - 1);
}
return authServerUrl;
}

Set<String> getUnavailableAuthServerUrls() {
return unavailableAuthServerUrls;
}

Set<String> getAvailableAuthServerUrls() {
return availableAuthServerUrls;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.quarkus.it.keycloak;

import static io.quarkus.it.keycloak.OidcEventObserver.dropTrailingSlash;

import java.util.stream.Collectors;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkus.oidc.runtime.OidcConfig;

@Path("/oidc-event")
public class OidcEventResource {

private final OidcEventObserver oidcEventObserver;
private final String expectedAuthServerUrl;

public OidcEventResource(OidcEventObserver oidcEventObserver, OidcConfig oidcConfig) {
this.expectedAuthServerUrl = dropTrailingSlash(oidcConfig.defaultTenant.authServerUrl.get());
this.oidcEventObserver = oidcEventObserver;
}

@Path("/unavailable-auth-server-urls")
@GET
public String unavailableAuthServerUrls() {
return oidcEventObserver
.getUnavailableAuthServerUrls()
.stream()
.sorted(String::compareTo)
.collect(Collectors.joining("-"));
}

@Path("/available-auth-server-urls")
@GET
public String availableAuthServerUrls() {
return oidcEventObserver
.getAvailableAuthServerUrls()
.stream()
.sorted(String::compareTo)
.collect(Collectors.joining("-"));
}

@GET
@Path("/expected-auth-server-url")
public String getExpectedAuthServerUrl() {
return expectedAuthServerUrl;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
import java.util.HashSet;
import java.util.Set;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.smallrye.jwt.build.Jwt;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@QuarkusTest
public class BearerTokenOidcRecoveredTest {

@Order(1)
@Test
public void testOidcRecoveredWithDiscovery() {
String token = getAccessToken("alice", new HashSet<>(Arrays.asList("user", "admin")));
Expand All @@ -39,6 +45,7 @@ public void testOidcRecoveredWithDiscovery() {
}
}

@Order(2)
@Test
public void testOidcRecoveredWithNoDiscovery() {
String token = getAccessToken("alice", new HashSet<>(Arrays.asList("user", "admin")));
Expand Down Expand Up @@ -72,4 +79,15 @@ private String getAccessToken(String userName, Set<String> groups) {
.keyId("1")
.sign();
}

@Order(3)
@Test
public void assertOidcServerAvailabilityReported() {
String expectAuthServerUrl = RestAssured.get("/oidc-event/expected-auth-server-url").then().statusCode(200).extract()
.asString();
RestAssured.given().get("/oidc-event/unavailable-auth-server-urls").then().statusCode(200)
.body(Matchers.is(expectAuthServerUrl));
RestAssured.given().get("/oidc-event/available-auth-server-urls").then().statusCode(200)
.body(Matchers.is(expectAuthServerUrl));
}
}

0 comments on commit cdbb8e1

Please sign in to comment.