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:
OidcClient
s as you want. These clients are gathered in the Clients
component which has been designed to support a lot of clients.OidcClient
on the fly, but at the startup if this is possible. You'll get better performance.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.