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) {

  }
}