javaopenid-connectjava-ee-8pac4j

Implementing multi-tenant authentication with Pac4J and OpenID Connect in JavaEE


I am currently working on implementing a multi-tenant application in JavaEE, where I am using Pac4J as the authentication framework and an OpenID Connect Identity provider. Each tenant in my application is identified by a unique identifier provided in the HTTP Header X-TENANT-ID. My goal is to authenticate users based on this tenant ID using Pac4J.

After doing some research, I believe I have come up with a possible approach, but I would like to gather some feedback and suggestions from the community. My plan is to generate a separate OidcClient for each tenant and then dynamically select the appropriate client based on the tenant ID extracted from the HTTP Header. However, this approach would result in loading around 100 to 200 clients. The corresponding ForceLoginFilter would look like this:

import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.pac4j.core.client.Client;
import org.pac4j.core.context.CallContext;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.exception.http.HttpAction;
import org.pac4j.jee.config.AbstractConfigFilter;
import org.pac4j.jee.context.JEEContext;
import org.pac4j.jee.context.session.JEESessionStore;
import org.pac4j.jee.http.adapter.JEEHttpActionAdapter;

import java.io.IOException;

public class ForceLoginFilter extends AbstractConfigFilter {

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {
    }

    @Override
    protected void internalFilter(final HttpServletRequest request, final HttpServletResponse response,
                                  final FilterChain chain) throws IOException, ServletException {

        final JEEContext context = new JEEContext(request, response);
        HttpAction action;
        try {
            final String tenantId = context.getRequestHeader("X-TENANT-ID").orElseThrow(() -> new TechnicalException("No tenant provided"));
            final Client client = getSharedConfig().getClients().findClient(tenantId).orElseThrow(() -> new TechnicalException("No client found"));
            action = client.getRedirectionAction(new CallContext(context, JEESessionStore.INSTANCE)).get();
        } catch (final HttpAction e) {
            action = e;
        }
        JEEHttpActionAdapter.INSTANCE.adapt(action, context);
    }
}

Another option would be to generate such an OidcClient on the fly. Like this:

public class ForceLoginFilter extends AbstractConfigFilter {

    /**
     * The OIDC configuration would be placed in an external file, but for the sake of simplicity, it is hardcoded here.
     */
    final KeycloakOidcConfiguration configuration;
    public ModiefiedForceLoginFilter() {
        configuration = new KeycloakOidcConfiguration();
        configuration.setClientId("client");
        configuration.setBaseUri("http://localhost:8080");
        configuration.setSecret("secret");
    }


    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {
    }

    @Override
    protected void internalFilter(final HttpServletRequest request, final HttpServletResponse response,
                                  final FilterChain chain) throws IOException, ServletException {

        final JEEContext context = new JEEContext(request, response);
        HttpAction action;
        try {
            final String tenantId = context.getRequestHeader("X-TENANT-ID").orElseThrow(() -> new TechnicalException("No tenant provided"));
            final Client client = new OidcClient(configuration.withRealm(tenantId));
            action = client.getRedirectionAction(new CallContext(context, JEESessionStore.INSTANCE)).get();
        } catch (final HttpAction e) {
            action = e;
        }
        JEEHttpActionAdapter.INSTANCE.adapt(action, context);
    }
}

Here are my specific questions:

  1. Is generating a separate OpenIDConnectClient for each tenant a feasible approach in terms of performance and scalability?
  2. Are there any best practices or alternative solutions for implementing multi-tenant authentication with Pac4J and OpenID Connect in JavaEE?
  3. Is there a better way to handle the tenant identification process, such as using a different Pac4J feature or a custom implementation? I would appreciate any insights or examples related to this aspect.

Solution

    1. Yes, you can have almost as many OidcClients as you want. These clients are gathered in the Clients component which has been designed to support a lot of clients.
    2. You should not create an OidcClient on the fly, but at the startup if this is possible. You'll get better performance.
    3. You should not create a ForceLoginFilter, but re-use the existing SecurityFilter and set a custom ClientFinder to replace the DefaultSecurityClientFinder: this CustomSecurityClientFinder would return the client based on the X-TENANT-ID HTTP header.