Skip to content

Commit

Permalink
Register BackChannelLogoutHandlers in OidcRecorder
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilKes committed Sep 14, 2024
1 parent dac2bb6 commit aef1ab7
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
import io.quarkus.oidc.TokenIntrospectionCache;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.UserInfoCache;
import io.quarkus.oidc.runtime.BackChannelLogoutHandler;
import io.quarkus.oidc.runtime.DefaultTenantConfigResolver;
import io.quarkus.oidc.runtime.DefaultTokenIntrospectionUserInfoCache;
import io.quarkus.oidc.runtime.DefaultTokenStateManager;
Expand All @@ -87,6 +86,7 @@
import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorBindingBuildItem;
import io.quarkus.vertx.http.deployment.HttpAuthMechanismAnnotationBuildItem;
import io.quarkus.vertx.http.deployment.SecurityInformationBuildItem;
import io.quarkus.vertx.http.deployment.VertxWebRouterBuildItem;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.smallrye.jwt.auth.cdi.ClaimValueProducer;
import io.smallrye.jwt.auth.cdi.CommonJwtProducer;
Expand Down Expand Up @@ -177,7 +177,6 @@ public void additionalBeans(BuildProducer<AdditionalBeanBuildItem> additionalBea
.addBeanClass(DefaultTenantConfigResolver.class)
.addBeanClass(DefaultTokenStateManager.class)
.addBeanClass(OidcSessionImpl.class)
.addBeanClass(BackChannelLogoutHandler.class)
.addBeanClass(AzureAccessTokenCustomizer.class);
additionalBeans.produce(builder.build());
}
Expand Down Expand Up @@ -324,10 +323,12 @@ public SyntheticBeanBuildItem setup(
OidcConfig config,
OidcRecorder recorder,
CoreVertxBuildItem vertxBuildItem,
TlsRegistryBuildItem tlsRegistryBuildItem) {
TlsRegistryBuildItem tlsRegistryBuildItem,
VertxWebRouterBuildItem routerBuildItem) {
return SyntheticBeanBuildItem.configure(TenantConfigBean.class).unremovable().types(TenantConfigBean.class)
.supplier(recorder.createTenantConfigBean(config, vertxBuildItem.getVertx(),
tlsRegistryBuildItem.registry(), detectUserInfoRequired(beanRegistration)))
tlsRegistryBuildItem.registry(), detectUserInfoRequired(beanRegistration),
routerBuildItem.getMainRouter()))
.destroyer(TenantConfigBean.Destroyer.class)
.scope(Singleton.class) // this should have been @ApplicationScoped but fails for some reason
.setRuntimeInit()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
import java.util.Map;
import java.util.function.Consumer;

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

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

import io.quarkus.arc.Arc;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.SecurityEvent.Type;
Expand All @@ -20,152 +18,127 @@
import io.vertx.core.MultiMap;
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 {
public class BackChannelLogoutHandler implements Handler<RoutingContext> {
private static final Logger LOG = Logger.getLogger(BackChannelLogoutHandler.class);
private static final String SLASH = "/";

@Inject
DefaultTenantConfigResolver resolver;

private final OidcConfig oidcConfig;

public BackChannelLogoutHandler(OidcConfig oidcConfig) {
this.oidcConfig = oidcConfig;
}

public void setup(@Observes Router router) {
addRoute(router, oidcConfig.defaultTenant);

for (OidcTenantConfig oidcTenantConfig : oidcConfig.namedTenants.values()) {
addRoute(router, oidcTenantConfig);
}
}
private final DefaultTenantConfigResolver resolver;
private final OidcTenantConfig oidcTenantConfig;

private void addRoute(Router router, OidcTenantConfig oidcTenantConfig) {
if (oidcTenantConfig.isTenantEnabled() && oidcTenantConfig.logout.backchannel.path.isPresent()) {
router.route(oidcTenantConfig.logout.backchannel.path.get())
.handler(new RouteHandler(oidcTenantConfig));
}
public BackChannelLogoutHandler(OidcTenantConfig oidcTenantConfig) {
this.oidcTenantConfig = oidcTenantConfig;
this.resolver = Arc.container().instance(DefaultTenantConfigResolver.class).get();
}

class RouteHandler implements Handler<RoutingContext> {
private final OidcTenantConfig oidcTenantConfig;

RouteHandler(OidcTenantConfig oidcTenantConfig) {
this.oidcTenantConfig = oidcTenantConfig;
@Override
public void handle(RoutingContext context) {
LOG.debugf("Back channel logout request for the tenant %s received", oidcTenantConfig.getTenantId().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.getTenantId().get(), requestPath);
context.response().setStatusCode(400);
context.response().end();
return;
}

@Override
public void handle(RoutingContext context) {
LOG.debugf("Back channel logout request for the tenant %s received", oidcTenantConfig.getTenantId().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.getTenantId().get(), requestPath);
context.response().setStatusCode(400);
context.response().end();
return;
}

if (OidcUtils.isFormUrlEncodedRequest(context)) {
OidcUtils.getFormUrlEncodedData(context)
.subscribe().with(new Consumer<MultiMap>() {
@Override
public void accept(MultiMap form) {
if (OidcUtils.isFormUrlEncodedRequest(context)) {
OidcUtils.getFormUrlEncodedData(context)
.subscribe().with(new Consumer<MultiMap>() {
@Override
public void accept(MultiMap form) {

String encodedLogoutToken = form.get(OidcConstants.BACK_CHANNEL_LOGOUT_TOKEN);
if (encodedLogoutToken == null) {
LOG.debug("Back channel logout token is missing");
context.response().setStatusCode(400);
} else {
try {
// Do the general validation of the logout token now, compare with the IDToken later
// Check the signature, as well the issuer and audience if it is configured
TokenVerificationResult result = tenantContext.provider
.verifyLogoutJwtToken(encodedLogoutToken);

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

String encodedLogoutToken = form.get(OidcConstants.BACK_CHANNEL_LOGOUT_TOKEN);
if (encodedLogoutToken == null) {
LOG.debug("Back channel logout token is missing");
context.response().setStatusCode(400);
} else {
try {
// Do the general validation of the logout token now, compare with the IDToken later
// Check the signature, as well the issuer and audience if it is configured
TokenVerificationResult result = tenantContext.provider
.verifyLogoutJwtToken(encodedLogoutToken);

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

if (resolver.isSecurityEventObserved()) {
SecurityEventHelper.fire(resolver.getSecurityEvent(),
new SecurityEvent(Type.OIDC_BACKCHANNEL_LOGOUT_INITIATED,
Map.of(OidcConstants.BACK_CHANNEL_LOGOUT_TOKEN, result)));
}
context.response().setStatusCode(200);
} else {
context.response().setStatusCode(400);
if (resolver.isSecurityEventObserved()) {
SecurityEventHelper.fire(resolver.getSecurityEvent(),
new SecurityEvent(Type.OIDC_BACKCHANNEL_LOGOUT_INITIATED,
Map.of(OidcConstants.BACK_CHANNEL_LOGOUT_TOKEN, result)));
}
} catch (InvalidJwtException e) {
LOG.debug("Back channel logout token is invalid");
context.response().setStatusCode(200);
} else {
context.response().setStatusCode(400);

}
} catch (InvalidJwtException e) {
LOG.debug("Back channel logout token is invalid");
context.response().setStatusCode(400);

}
context.response().end();
}
context.response().end();
}

});
});

} else {
LOG.debug("HTTP POST and " + HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()
+ " content type must be used with the Back channel logout request");
context.response().setStatusCode(400);
context.response().end();
}
} else {
LOG.debug("HTTP POST and " + HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()
+ " content type must be used with the Back channel logout request");
context.response().setStatusCode(400);
context.response().end();
}
}

private boolean verifyLogoutTokenClaims(TokenVerificationResult result) {
// events
JsonObject events = result.localVerificationResult.getJsonObject(OidcConstants.BACK_CHANNEL_EVENTS_CLAIM);
if (events == null || events.getJsonObject(OidcConstants.BACK_CHANNEL_EVENT_NAME) == null) {
LOG.debug("Back channel logout token does not have a valid 'events' claim");
return false;
}
if (!result.localVerificationResult.containsKey(oidcTenantConfig.logout.backchannel.logoutTokenKey)) {
LOG.debugf("Back channel logout token does not have %s", oidcTenantConfig.logout.backchannel.logoutTokenKey);
return false;
}
if (result.localVerificationResult.containsKey(Claims.nonce.name())) {
LOG.debug("Back channel logout token must not contain 'nonce' claim");
return false;
}

return true;
private boolean verifyLogoutTokenClaims(TokenVerificationResult result) {
// events
JsonObject events = result.localVerificationResult.getJsonObject(OidcConstants.BACK_CHANNEL_EVENTS_CLAIM);
if (events == null || events.getJsonObject(OidcConstants.BACK_CHANNEL_EVENT_NAME) == null) {
LOG.debug("Back channel logout token does not have a valid 'events' claim");
return false;
}
if (!result.localVerificationResult.containsKey(oidcTenantConfig.logout.backchannel.logoutTokenKey)) {
LOG.debugf("Back channel logout token does not have %s", oidcTenantConfig.logout.backchannel.logoutTokenKey);
return false;
}
if (result.localVerificationResult.containsKey(Claims.nonce.name())) {
LOG.debug("Back channel logout token must not contain 'nonce' claim");
return false;
}

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 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;
}
return null;
}

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

private String getRootPath() {
Expand Down
Loading

0 comments on commit aef1ab7

Please sign in to comment.