FrontChannelLogoutHandler.java
package org.keycloak.protocol.oidc;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.utils.StringUtil;
public class FrontChannelLogoutHandler {
public static FrontChannelLogoutHandler current(KeycloakSession session) {
return (FrontChannelLogoutHandler) session.getAttribute(FrontChannelLogoutHandler.class.getName());
}
public static FrontChannelLogoutHandler currentOrCreate(KeycloakSession session, AuthenticatedClientSessionModel clientSession) {
FrontChannelLogoutHandler current = current(session);
if (current == null) {
return new FrontChannelLogoutHandler(session, clientSession);
}
return current;
}
private final KeycloakSession session;
private final String sid;
private final String issuer;
private final List<ClientInfo> clients = new ArrayList<>();
private String logoutRedirectUri;
private FrontChannelLogoutHandler(KeycloakSession session, AuthenticatedClientSessionModel clientSession) {
this.session = session;
this.sid = clientSession.getUserSession().getId();
this.issuer = clientSession.getNote(OIDCLoginProtocol.ISSUER);
this.session.setAttribute(getClass().getName(), this);
}
public void addClient(ClientModel client) {
clients.add(new ClientInfo(client));
}
public List<ClientInfo> getClients() {
return clients;
}
public String getLogoutRedirectUri() {
return logoutRedirectUri;
}
public Response renderLogoutPage(String redirectUri) {
configureCSP();
this.logoutRedirectUri = redirectUri;
return session.getProvider(LoginFormsProvider.class).createFrontChannelLogoutPage();
}
private void configureCSP() {
StringBuilder allowFrameSrc = new StringBuilder();
for (ClientInfo client : clients) {
allowFrameSrc.append(client.frontChannelLogoutUrl.getAuthority()).append(' ');
}
session.getProvider(SecurityHeadersProvider.class).options().allowAnyFrameAncestor();
session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(allowFrameSrc.toString());
}
private URI createFrontChannelLogoutUrl(ClientModel client) {
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientModel(client);
String frontChannelLogoutUrl = config.getFrontChannelLogoutUrl();
if (StringUtil.isBlank(frontChannelLogoutUrl)) {
frontChannelLogoutUrl = client.getBaseUrl();
}
if (frontChannelLogoutUrl == null) {
throw new RuntimeException("Client [" + client.getClientId() + "] does not have a valid frontend logout URL");
}
UriBuilder builder = UriBuilder.fromUri(frontChannelLogoutUrl);
if (config.isFrontChannelLogoutSessionRequired()) {
builder.queryParam("sid", FrontChannelLogoutHandler.this.sid);
builder.queryParam("iss", FrontChannelLogoutHandler.this.issuer);
}
return builder.build();
}
public class ClientInfo {
private final ClientModel client;
private final URI frontChannelLogoutUrl;
public ClientInfo(ClientModel client) {
this.client = client;
this.frontChannelLogoutUrl = createFrontChannelLogoutUrl(client);
}
public String getFrontChannelLogoutUrl() {
return frontChannelLogoutUrl.toString();
}
public String getName() {
String name = client.getName();
if (name == null) {
return client.getClientId();
}
return name;
}
}
}