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
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
import io.quarkus.tls.deployment.spi.TlsRegistryBuildItem;
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorBindingBuildItem;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import io.quarkus.vertx.http.deployment.HttpAuthMechanismAnnotationBuildItem;
import io.quarkus.vertx.http.deployment.HttpSecurityUtils;
import io.quarkus.vertx.http.deployment.PreRouterFinalizationBuildItem;
Expand All @@ -120,6 +121,8 @@
import io.smallrye.jwt.auth.cdi.CommonJwtProducer;
import io.smallrye.jwt.auth.cdi.JsonValueProducer;
import io.smallrye.jwt.auth.cdi.RawClaimTypeProducer;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;

@BuildSteps(onlyIf = OidcBuildStep.IsEnabled.class)
public class OidcBuildStep {
Expand Down Expand Up @@ -482,6 +485,13 @@ public void registerHealthCheck(OidcBuildTimeConfig config, BuildProducer<Health
}
}

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
FilterBuildItem registerBackChannelLogoutHandler(BeanContainerBuildItem beanContainerBuildItem, OidcRecorder recorder) {
Handler<RoutingContext> handler = recorder.getBackChannelLogoutHandler(beanContainerBuildItem.getValue());
return new FilterBuildItem(handler, FilterBuildItem.AUTHORIZATION - 50);
}

private static boolean areEagerSecInterceptorsSupported(Capabilities capabilities,
VertxHttpBuildTimeConfig httpBuildTimeConfig) {
if (httpBuildTimeConfig.auth().proactive()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,71 +1,170 @@
package io.quarkus.oidc.runtime;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

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

import org.eclipse.microprofile.jwt.Claims;
import org.jboss.logging.Logger;
import org.jose4j.jwt.consumer.InvalidJwtException;

import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.SecurityEvent.Type;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;

public class BackChannelLogoutHandler {
@Singleton
public final class BackChannelLogoutHandler implements Handler<RoutingContext> {
private static final Logger LOG = Logger.getLogger(BackChannelLogoutHandler.class);
private static final String SLASH = "/";
private final DefaultTenantConfigResolver resolver;
private volatile ImmutablePathMatcher<Handler<RoutingContext>> pathMatcher;

void setup(@Observes Router router, DefaultTenantConfigResolver resolver) {
final TenantConfigBean tenantConfigBean = resolver.getTenantConfigBean();
addRoute(router, tenantConfigBean.getDefaultTenant().oidcConfig(), resolver);
for (var nameToOidcTenantConfig : tenantConfigBean.getStaticTenantsConfig().values()) {
if (nameToOidcTenantConfig.oidcConfig() != null) {
addRoute(router, nameToOidcTenantConfig.oidcConfig(), resolver);
record NewBackChannelLogoutPath() {
}

BackChannelLogoutHandler(DefaultTenantConfigResolver resolver) {
this.resolver = resolver;
this.pathMatcher = null;
}

@Override
public void handle(RoutingContext routingContext) {
var matcher = pathMatcher;
if (matcher != null) {
Handler<RoutingContext> routeHandler = matcher.match(routingContext.normalizedPath()).getValue();
if (routeHandler != null) {
routeHandler.handle(routingContext);
return;
}
}

routingContext.next();
}

// hook up to the router because then we the tenant config bean is surely ready
void createPathMatcher(@Observes Router ignored) {
createOrUpdatePathMatcher();
}

synchronized void updatePathMatcher(@Observes NewBackChannelLogoutPath ignored, Vertx vertx) {
Set<String> currentTenantIds = createOrUpdatePathMatcher();
clearCache(vertx, currentTenantIds);
}

void clearCacheOnShutdown(@Observes ShutdownEvent event, Vertx vertx) {
clearCache(vertx, null);
}

private static void addRoute(Router router, OidcTenantConfig oidcTenantConfig, DefaultTenantConfigResolver resolver) {
if (oidcTenantConfig.tenantEnabled() && oidcTenantConfig.logout().backchannel().path().isPresent()) {
router.route(oidcTenantConfig.logout().backchannel().path().get())
.handler(new RouteHandler(oidcTenantConfig, resolver));
private void clearCache(Vertx vertx, Set<String> currentTenantIds) {
if (currentTenantIds == null) {
// clear all as currently we have no tenants with a back-channel logout path
for (BackChannelLogoutTokenCache cache : resolver.getBackChannelLogoutTokens().values()) {
cache.shutdown(vertx);
}
resolver.getBackChannelLogoutTokens().clear();
} else {
// clear only the ones that currently don't have a logout back-channel
Set<String> cachedTenantIds = new HashSet<>(resolver.getBackChannelLogoutTokens().keySet());
for (String cachedTenantId : cachedTenantIds) {
if (!currentTenantIds.contains(cachedTenantId)) {
var cache = resolver.getBackChannelLogoutTokens().remove(cachedTenantId);
if (cache != null) {
cache.shutdown(vertx);
}
}
}
}
}

private static class RouteHandler implements Handler<RoutingContext> {
private final OidcTenantConfig oidcTenantConfig;
private Set<String> createOrUpdatePathMatcher() {
ImmutablePathMatcher.ImmutablePathMatcherBuilder<Handler<RoutingContext>> builder = null;
Map<String, OidcTenantConfig> pathCache = null;
Set<String> tenantIdCache = null;
for (TenantConfigContext configContext : resolver.getTenantConfigBean().getAllTenantConfigs()) {
if (configContext.ready() && configContext.oidcConfig().tenantEnabled()
&& configContext.oidcConfig().logout().backchannel().path().isPresent()) {
if (builder == null) {
builder = ImmutablePathMatcher.builder();
pathCache = new HashMap<>();
tenantIdCache = new HashSet<>();
}
String routePath = getTenantLogoutPath(configContext);
if (routePath.contains("*")) {
throw new IllegalStateException("Back-channel logout path cannot contain a wildcard '*' character");
}
OidcTenantConfig previousConfig = pathCache.put(routePath, configContext.oidcConfig());
tenantIdCache.add(configContext.oidcConfig().tenantId().get());
if (previousConfig == null) {
Handler<RoutingContext> routeHandler = new RouteHandler(configContext, resolver);
builder.addPath(routePath, routeHandler);
} else {
String previousTenantId = previousConfig.tenantId().get();
String currentTenantId = configContext.oidcConfig().tenantId().get();
// maybe invalid state, but technically it could happen that some produces a static tenant with
// a same id as a dynamic tenant
if (!previousTenantId.equals(currentTenantId)) {
String errorMessage = "OIDC tenants '%s' and '%s' shares same back-channel logout path '%s', which is not supported"
.formatted(previousTenantId, currentTenantId, routePath);
LOG.error(errorMessage);
throw new OIDCException(errorMessage);
}
}
}
}
if (builder != null) {
pathMatcher = builder.build();
} else {
pathMatcher = null;
}
return tenantIdCache;
}

private String getTenantLogoutPath(TenantConfigContext tenant) {
return getRootPath() + tenant.oidcConfig().logout().backchannel().path().orElse(null);
}

private String getRootPath() {
// Prepend '/' if it is not present
String rootPath = OidcCommonUtils.prependSlash(resolver.getRootPath());
// Strip trailing '/' if the length is > 1
if (rootPath.length() > 1 && rootPath.endsWith("/")) {
rootPath = rootPath.substring(rootPath.length() - 1);
}
// if it is only '/' then return an empty value
return SLASH.equals(rootPath) ? "" : rootPath;
}

private static final class RouteHandler implements Handler<RoutingContext> {
private final TenantConfigContext tenantContext;
private final DefaultTenantConfigResolver resolver;
private final String tenantId;

RouteHandler(OidcTenantConfig oidcTenantConfig, DefaultTenantConfigResolver resolver) {
this.oidcTenantConfig = oidcTenantConfig;
private RouteHandler(TenantConfigContext tenantContext, DefaultTenantConfigResolver resolver) {
this.tenantContext = tenantContext;
this.resolver = resolver;
this.tenantId = tenantContext.oidcConfig().tenantId().get();
}

@Override
public void handle(RoutingContext context) {
LOG.debugf("Back channel logout request for the tenant %s received", oidcTenantConfig.tenantId().get());
final String requestPath = context.request().path();
final TenantConfigContext tenantContext = getTenantConfigContext(requestPath);
if (tenantContext == null) {
LOG.errorf(
"Tenant configuration for the tenant %s is not available "
+ "or does not match the backchannel logout path %s",
oidcTenantConfig.tenantId().get(), requestPath);
context.response().setStatusCode(400);
context.response().end();
return;
}

LOG.debugf("Back channel logout request for the tenant %s received", tenantId);
if (OidcUtils.isFormUrlEncodedRequest(context)) {
OidcUtils.getFormUrlEncodedData(context)
.subscribe().with(new Consumer<MultiMap>() {
Expand All @@ -85,13 +184,14 @@ public void accept(MultiMap form) {

if (verifyLogoutTokenClaims(result)) {
String key = result.localVerificationResult
.getString(oidcTenantConfig.logout().backchannel().logoutTokenKey());
BackChannelLogoutTokenCache tokens = resolver
.getBackChannelLogoutTokens().get(oidcTenantConfig.tenantId().get());
.getString(
tenantContext.oidcConfig().logout().backchannel().logoutTokenKey());
BackChannelLogoutTokenCache tokens = resolver.getBackChannelLogoutTokens()
.get(tenantId);
if (tokens == null) {
tokens = new BackChannelLogoutTokenCache(oidcTenantConfig, context.vertx());
resolver.getBackChannelLogoutTokens().put(oidcTenantConfig.tenantId().get(),
tokens);
tokens = new BackChannelLogoutTokenCache(tenantContext.oidcConfig(),
context.vertx());
resolver.getBackChannelLogoutTokens().put(tenantId, tokens);
}
tokens.addTokenVerification(key, result);

Expand Down Expand Up @@ -130,9 +230,10 @@ private boolean verifyLogoutTokenClaims(TokenVerificationResult result) {
LOG.debug("Back channel logout token does not have a valid 'events' claim");
return false;
}
if (!result.localVerificationResult.containsKey(oidcTenantConfig.logout().backchannel().logoutTokenKey())) {
if (!result.localVerificationResult
.containsKey(tenantContext.oidcConfig().logout().backchannel().logoutTokenKey())) {
LOG.debugf("Back channel logout token does not have %s",
oidcTenantConfig.logout().backchannel().logoutTokenKey());
tenantContext.oidcConfig().logout().backchannel().logoutTokenKey());
return false;
}
if (result.localVerificationResult.containsKey(Claims.nonce.name())) {
Expand All @@ -142,34 +243,5 @@ private boolean verifyLogoutTokenClaims(TokenVerificationResult result) {

return true;
}

private TenantConfigContext getTenantConfigContext(final String requestPath) {
if (isMatchingTenant(requestPath, resolver.getTenantConfigBean().getDefaultTenant())) {
return resolver.getTenantConfigBean().getDefaultTenant();
}
for (TenantConfigContext tenant : resolver.getTenantConfigBean().getStaticTenantsConfig().values()) {
if (isMatchingTenant(requestPath, tenant)) {
return tenant;
}
}
return null;
}

private boolean isMatchingTenant(String requestPath, TenantConfigContext tenant) {
return tenant.oidcConfig().tenantEnabled()
&& tenant.oidcConfig().tenantId().get().equals(oidcTenantConfig.tenantId().get())
&& requestPath.equals(getRootPath() + tenant.oidcConfig().logout().backchannel().path().orElse(null));
}

private String getRootPath() {
// Prepend '/' if it is not present
String rootPath = OidcCommonUtils.prependSlash(resolver.getRootPath());
// Strip trailing '/' if the length is > 1
if (rootPath.length() > 1 && rootPath.endsWith("/")) {
rootPath = rootPath.substring(rootPath.length() - 1);
}
// if it is only '/' then return an empty value
return SLASH.equals(rootPath) ? "" : rootPath;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package io.quarkus.oidc.runtime;

import jakarta.enterprise.event.Observes;

import io.quarkus.runtime.ShutdownEvent;
import io.vertx.core.Vertx;

public class BackChannelLogoutTokenCache {
Expand All @@ -27,7 +24,7 @@ public boolean containsTokenVerification(String token) {
return cache.containsKey(token);
}

void shutdown(@Observes ShutdownEvent event, Vertx vertx) {
void shutdown(Vertx vertx) {
cache.stopTimer(vertx);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ boolean isEnableHttpForwardedPrefix() {
return enableHttpForwardedPrefix;
}

public Map<String, BackChannelLogoutTokenCache> getBackChannelLogoutTokens() {
Map<String, BackChannelLogoutTokenCache> getBackChannelLogoutTokens() {
return backChannelLogoutTokens;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@

import javax.crypto.SecretKey;

import jakarta.enterprise.event.Event;

import org.jboss.logging.Logger;

import io.quarkus.arc.Arc;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcRedirectFilter;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.Redirect;
import io.quarkus.oidc.runtime.BackChannelLogoutHandler.NewBackChannelLogoutPath;
import io.smallrye.mutiny.Uni;

final class LazyTenantConfigContext implements TenantConfigContext {
Expand All @@ -32,7 +36,14 @@ public Uni<TenantConfigContext> initialize() {
if (!delegate.ready()) {
LOG.debugf("Tenant '%s' is not initialized yet, trying to create OIDC connection now",
delegate.oidcConfig().tenantId().get());
return staticTenantCreator.get().invoke(ctx -> LazyTenantConfigContext.this.delegate = ctx);
return staticTenantCreator.get().invoke(ctx -> {
LazyTenantConfigContext.this.delegate = ctx;
if (ctx.ready() && ctx.oidcConfig().logout().backchannel().path().isPresent()) {
Event<NewBackChannelLogoutPath> event = Arc.container().beanManager().getEvent()
.select(NewBackChannelLogoutPath.class);
event.fire(new NewBackChannelLogoutPath());
}
});
}
return Uni.createFrom().item(delegate);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import io.quarkus.arc.Arc;
import io.quarkus.arc.SyntheticCreationalContext;
import io.quarkus.arc.runtime.BeanContainer;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.Oidc;
Expand All @@ -34,6 +35,7 @@
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.tls.TlsConfigurationRegistry;
import io.smallrye.mutiny.Uni;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.ext.web.RoutingContext;

Expand Down Expand Up @@ -174,6 +176,10 @@ public void accept(RoutingContext routingContext) {
};
}

public Handler<RoutingContext> getBackChannelLogoutHandler(BeanContainer beanContainer) {
return beanContainer.beanInstance(BackChannelLogoutHandler.class);
}

private static final class TenantSpecificOidcIdentityProvider extends OidcIdentityProvider
implements TenantIdentityProvider {

Expand Down
Loading
Loading