Custom SSO Solution
Our built-in solutions may not fit your needs - but you may implement your own Single Sign-on solution by implementing a Tomcat Valve and register this valve in context.xml. A valve is something which will be executed for every request sent to the Axon Ivy Engine.
This is our Single Sign-on valve. Use it as template and adapt it your needs:
package ch.ivyteam.ivy.webserver.security;
import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.apache.commons.lang3.StringUtils;
import ch.ivyteam.api.PublicAPI;
import ch.ivyteam.ivy.configuration.restricted.ConfigKey;
import ch.ivyteam.ivy.configuration.restricted.IConfiguration;
import ch.ivyteam.ivy.security.ISecurityContextRepository;
import ch.ivyteam.ivy.security.restricted.ISecurityContextInternal;
import ch.ivyteam.ivy.webserver.IWebServer;
/**
* <p>
* <strong style="color:red"> Only use this Valve if you exclusively access Axon
* Ivy over the WebApplication Firewall. Otherwise this will be a security
* hole.</strong>
* </p>
*
* This Valve is useful if Axon Ivy is protected by a WebApplication Firewall
* (WAF) with an integrated Identity and Access Management (IAM). Those systems
* will authenticate and authorize users. The identified user is then sent from
* the WAF to Axon Ivy using a HTTP request header.
*
* <pre>
* WebBrowser {@literal ==>} WAF {@literal ==>} Axon Ivy
*
* ^ |
* | |
* v v
*
* IAM {@literal ==>} Active Directory
* </pre>
*
* @since 6.6
*/
@PublicAPI
public class SingleSignOnValve extends ValveBase {
public static final String DEFAULT_HEADER_NAME = "X-Forwarded-User";
private static final ConfigKey SSO_KEY = ConfigKey.create("SSO");
private static final ConfigKey SSO_ENABLED_KEY = SSO_KEY.append("Enabled");
private static final ConfigKey SSO_USERHEADER_KEY = SSO_KEY.append("UserHeader");
private final Map<String, Data> cache = new ConcurrentHashMap<>();
public SingleSignOnValve() {
super(true);
}
/**
* This implementation reads the user from the HTTP header field user.
*
* @see org.apache.catalina.Valve#invoke(org.apache.catalina.connector.Request,
* org.apache.catalina.connector.Response)
*/
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
var context = request.getContext();
if (context == null) {
getNext().invoke(request, response);
return;
}
var securityCtxName = IWebServer.getIvySecurityContextName(context.getServletContext());
if (StringUtils.isBlank(securityCtxName)) {
getNext().invoke(request, response);
return;
}
var data = cache.computeIfAbsent(securityCtxName, this::create);
if (data.enabled()) {
var userName = request.getHeader(data.headerName());
if (StringUtils.isNotBlank(userName)) {
var principal = new UserPrincipal(userName);
request.setUserPrincipal(principal);
}
}
getNext().invoke(request, response);
}
private Data create(String securityCtxName) {
var securityContext = (ISecurityContextInternal) ISecurityContextRepository.instance().get(securityCtxName);
if (securityContext == null) {
return new Data(false, "");
}
var prefix = securityContext.config().prefix();
var cfg = IConfiguration.instance();
var enabledKey = prefix.append(SSO_ENABLED_KEY);
var enabled = cfg.get(enabledKey, boolean.class).orElse(false);
var headerNameKey = prefix.append(SSO_USERHEADER_KEY);
var headerName = cfg.get(headerNameKey).orElse(DEFAULT_HEADER_NAME);
return new Data(enabled, headerName);
}
private static final class UserPrincipal implements Principal {
private final String userName;
UserPrincipal(String userName) {
this.userName = userName;
}
@Override
public String getName() {
return userName;
}
}
record Data(boolean enabled, String headerName) {
}
}