Skip to content

Commit

Permalink
Add support for OIDC BackChannel Logout
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Apr 4, 2022
1 parent 6cc12f6 commit a28d221
Show file tree
Hide file tree
Showing 15 changed files with 418 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,53 @@ quarkus.oidc.logout.post-logout-uri-param=returnTo
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
----

[[back-channel-logout]]
==== Back-Channel Logout

link:https://openid.net/specs/openid-connect-backchannel-1_0.html[Back-Channel Logout] is used by OpenId Connect providers to logout the current user from all the applications this user is currently logged in, bypassing the user agent.

You can configure Quarkus to support `Back-Channel Logout` as follows:

[source,properties]
----
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.back-channel-logout.path=/back-channel-logout
----

Absolute `Back-Channel Logout` URL is calculated by adding `quarkus.oidc.back-channel-logout.path` to the current endpoint URL, for example, `http://localhost:8080/back-channel-logout`. You will need to configure this URL in the Admin Console of your OpenId Connect Provider.

Note you may also have to authenticate the incoming back-channel logout request using a path-based authentication mechanism configuration, for example:

[source,properties]
----
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.back-channel-logout.path=/back-channel-logout
# Request OpenId Connect Authorization Code authentication for all the endpoint paths starting from '/services*'
quarkus.http.auth.permission.codeflow.paths=/service/*
quarkus.http.auth.permission.codeflow.policy=authenticated
quarkus.http.auth.permission.codeflow.auth-mechanism=code
# Request Basic Authentication for Back-Channel Logout requests
quarkus.http.auth.permission.backchannellogout.paths=/back-channel-logout
quarkus.http.auth.permission.backchannellogout.policy=authenticated
quarkus.http.auth.permission.backchannellogout.auth-mechanism=basic
quarkus.http.auth.basic=true
quarkus.security.users.embedded.enabled=true
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.oidc-backchannel-logout-manager=oidc-backchannel-logout-manager-password
----

[[local-logout]]
==== Local Logout

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public void run() {
List<Credential> credentials = new ArrayList<>();
credentials.add(passwordCred);
String rawRoles = roleInfo.get(user);
String[] roles = rawRoles.split(",");
String[] roles = rawRoles != null ? rawRoles.split(",") : new String[0];
Attributes attributes = new MapAttributes();
for (String role : roles) {
attributes.addLast("groups", role);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,9 @@ public final class OidcConstants {

public static final String PKCE_CODE_CHALLENGE_METHOD = "code_challenge_method";
public static final String PKCE_CODE_CHALLENGE_S256 = "S256";

public static final String BACK_CHANNEL_LOGOUT_TOKEN = "logout_token";
public static final String BACK_CHANNEL_EVENTS_CLAIM = "events";
public static final String BACK_CHANNEL_EVENT_NAME = "http://schemas.openid.net/event/backchannel-logout";
public static final String BACK_CHANNEL_LOGOUT_SID_CLAIM = "sid";
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.TokenIntrospectionCache;
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 Down Expand Up @@ -89,7 +90,8 @@ public void additionalBeans(BuildProducer<AdditionalBeanBuildItem> additionalBea
.addBeanClass(OidcIdentityProvider.class)
.addBeanClass(DefaultTenantConfigResolver.class)
.addBeanClass(DefaultTokenStateManager.class)
.addBeanClass(OidcSessionImpl.class);
.addBeanClass(OidcSessionImpl.class)
.addBeanClass(BackChannelLogoutHandler.class);
additionalBeans.produce(builder.build());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,17 @@ public class OidcTenantConfig extends OidcCommonConfig {
public Token token = new Token();

/**
* Logout configuration
* RP Initiated Logout configuration
*/
@ConfigItem
public Logout logout = new Logout();

/**
* Back-Channel Logout configuration
*/
@ConfigItem
public BackChannelLogout backChannelLogout = new BackChannelLogout();

/**
* Different options to configure authorization requests
*/
Expand Down Expand Up @@ -204,6 +210,23 @@ public void setPostLogoutUriParam(String postLogoutUriParam) {
}
}

@ConfigGroup
public static class BackChannelLogout {
/**
* The relative path of the Back-Channel Logout endpoint at the application.
*/
@ConfigItem
public Optional<String> path = Optional.empty();

public void setPath(Optional<String> path) {
this.path = path;
}

public String getPath() {
return path.get();
}
}

/**
* Default Authorization Code token state manager configuration
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package io.quarkus.oidc.runtime;

import java.util.function.Consumer;

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

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

import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.vertx.core.Handler;
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 {
private static final Logger LOG = Logger.getLogger(BackChannelLogoutHandler.class);

@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 void addRoute(Router router, OidcTenantConfig oidcTenantConfig) {
if (oidcTenantConfig.isTenantEnabled() && oidcTenantConfig.backChannelLogout.path.isPresent()) {
router.route(oidcTenantConfig.backChannelLogout.path.get()).handler(new RouteHandler(oidcTenantConfig));
}
}

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 TenantConfigContext tenantContext = getTenantConfigContext(context);
if (tenantContext == null) {
LOG.debugf(
"Tenant configuration for the tenant %s is not available or does not match the backchannel logout path",
oidcTenantConfig.getTenantId().get());
}

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
.verifyJwtToken(encodedLogoutToken);

if (verifyLogoutTokenClaims(result)) {
resolver.getBackChannelLogoutTokens().put(oidcTenantConfig.tenantId.get(),
result);
} else {
context.response().setStatusCode(400);
}
} catch (InvalidJwtException e) {
LOG.debug("Back channel logout token is invalid");
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(Claims.sub.name())
&& !result.localVerificationResult.containsKey(OidcConstants.BACK_CHANNEL_LOGOUT_SID_CLAIM)) {
LOG.debug("Back channel logout token does not have 'sub' or 'sid' claim");
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 TenantConfigContext getTenantConfigContext(RoutingContext context) {
String requestPath = context.request().path();
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.isTenantEnabled()
&& tenant.oidcConfig.getTenantId().get().equals(oidcTenantConfig.getTenantId().get())
&& requestPath.equals(tenant.oidcConfig.backChannelLogout.path.orElse(null));
}
}
}
Loading

0 comments on commit a28d221

Please sign in to comment.