You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ep...@apache.org on 2025/03/08 11:48:36 UTC

(solr) branch main updated: SOLR-17309: Enhance certificate based authentication plugin with flexible cert principal resolution (#3029)

This is an automated email from the ASF dual-hosted git repository.

epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 9d2118bafb8 SOLR-17309: Enhance certificate based authentication plugin with flexible cert principal resolution (#3029)
9d2118bafb8 is described below

commit 9d2118bafb885f983d033c85c8a3accac11c0faa
Author: Lamine <10...@users.noreply.github.com>
AuthorDate: Sat Mar 8 05:48:32 2025 -0600

    SOLR-17309: Enhance certificate based authentication plugin with flexible cert principal resolution (#3029)
    
    ---------
    
    Co-authored-by: Lamine Idjeraoui <li...@apple.com>
    Co-authored-by: Eric Pugh <ep...@opensourceconnections.com>
---
 solr/CHANGES.txt                                   |   2 +
 .../org/apache/solr/core/SolrResourceLoader.java   |   1 +
 .../org/apache/solr/security/CertAuthPlugin.java   |  86 ++++-
 .../solr/security/cert/CertPrincipalResolver.java  |  46 +++
 .../solr/security/cert/CertResolverPattern.java    | 131 +++++++
 .../org/apache/solr/security/cert/CertUtil.java    | 200 +++++++++++
 .../cert/PathBasedCertPrincipalResolver.java       | 155 ++++++++
 .../security/cert/PathBasedCertResolverBase.java   | 122 +++++++
 .../apache/solr/security/cert/package-info.java    |  19 +
 .../apache/solr/security/CertAuthPluginTest.java   |   2 +
 .../PathBasedCertPrincipalResolverTest.java        | 392 +++++++++++++++++++++
 .../pages/cert-authentication-plugin.adoc          | 389 +++++++++++++++++++-
 12 files changed, 1535 insertions(+), 10 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index c31de560b8f..f2bd8735fb3 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -165,6 +165,8 @@ New Features
 
 * SOLR-17656: New 'skipLeaderRecovery' replica property allows PULL replicas with existing indexes to immediately become ACTIVE (hossman)
 
+* SOLR-17309: Certificate based authentication plugin now has richer flexible cert principal resolution. (Lamine Idjeraoui via Eric Pugh)
+
 Improvements
 ---------------------
 * SOLR-15751: The v2 API now has parity with the v1 "COLSTATUS" and "segments" APIs, which can be used to fetch detailed information about 
diff --git a/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java b/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java
index ffd9bc7c8d2..3e1165e5899 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java
@@ -107,6 +107,7 @@ public class SolrResourceLoader
     "security.",
     "handler.admin.",
     "security.jwt.",
+    "security.cert.",
     "handler.sql.",
     "crossdc.handler.",
     "crossdc.update.processor."
diff --git a/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java
index ce59a240f91..fa8d08f70fa 100644
--- a/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java
@@ -16,17 +16,86 @@
  */
 package org.apache.solr.security;
 
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
 import java.security.cert.X509Certificate;
 import java.util.Map;
 import javax.servlet.FilterChain;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.apache.http.HttpHeaders;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.security.cert.CertPrincipalResolver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** An authentication plugin that sets principal based on the certificate subject */
 public class CertAuthPlugin extends AuthenticationPlugin {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static final String PARAM_PRINCIPAL_RESOLVER = "principalResolver";
+  private static final String PARAM_CLASS = "class";
+  private static final String PARAM_PARAMS = "params";
+
+  private static final CertPrincipalResolver DEFAULT_PRINCIPAL_RESOLVER =
+      certificate -> certificate.getSubjectX500Principal();
+  protected final CoreContainer coreContainer;
+  private CertPrincipalResolver principalResolver;
+
+  public CertAuthPlugin() {
+    this(null);
+  }
+
+  public CertAuthPlugin(CoreContainer coreContainer) {
+    this.coreContainer = coreContainer;
+  }
+
   @Override
-  public void init(Map<String, Object> pluginConfig) {}
+  public void init(Map<String, Object> pluginConfig) {
+    principalResolver =
+        resolveComponent(
+            pluginConfig,
+            PARAM_PRINCIPAL_RESOLVER,
+            CertPrincipalResolver.class,
+            DEFAULT_PRINCIPAL_RESOLVER,
+            "principalResolver");
+  }
+
+  @SuppressWarnings("unchecked")
+  private <T> T resolveComponent(
+      Map<String, Object> pluginConfig,
+      String configKey,
+      Class<T> clazz,
+      T defaultInstance,
+      String componentName) {
+    Map<String, Object> configMap = (Map<String, Object>) pluginConfig.get(configKey);
+    if (this.coreContainer == null) {
+      log.warn("No coreContainer configured. Using the default {}", componentName);
+      return defaultInstance;
+    }
+    if (configMap == null) {
+      log.warn("No {} configured. Using the default one", componentName);
+      return defaultInstance;
+    }
+
+    String className = (String) configMap.get(PARAM_CLASS);
+    if (StrUtils.isNullOrEmpty(className)) {
+      log.warn("No {} class configured. Using the default one", componentName);
+      return defaultInstance;
+    }
+    Map<String, Object> params = (Map<String, Object>) configMap.get(PARAM_PARAMS);
+    if (params == null) {
+      log.warn("No params found for {}. Using the default class", componentName);
+      return defaultInstance;
+    }
+
+    log.info("Found a {} class: {}", componentName, className);
+    return this.coreContainer
+        .getResourceLoader()
+        .newInstance(className, clazz, null, new Class<?>[] {Map.class}, new Object[] {params});
+  }
 
   @Override
   public boolean doAuthenticate(
@@ -35,15 +104,20 @@ public class CertAuthPlugin extends AuthenticationPlugin {
     X509Certificate[] certs =
         (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
     if (certs == null || certs.length == 0) {
-      numMissingCredentials.inc();
-      response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Certificate");
-      response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "require certificate");
-      return false;
+      return sendError(response, "require certificate");
     }
 
-    HttpServletRequest wrapped = wrapWithPrincipal(request, certs[0].getSubjectX500Principal());
+    HttpServletRequest wrapped =
+        wrapWithPrincipal(request, principalResolver.resolvePrincipal(certs[0]));
     numAuthenticated.inc();
     filterChain.doFilter(wrapped, response);
     return true;
   }
+
+  private boolean sendError(HttpServletResponse response, String msg) throws IOException {
+    numMissingCredentials.inc();
+    response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Certificate");
+    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, msg);
+    return false;
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/security/cert/CertPrincipalResolver.java b/solr/core/src/java/org/apache/solr/security/cert/CertPrincipalResolver.java
new file mode 100644
index 00000000000..090f3da72a9
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/cert/CertPrincipalResolver.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.security.cert;
+
+import java.security.Principal;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+/**
+ * Defines the interface for resolving a {@link Principal} from an X509 certificate. Implementations
+ * of this interface are responsible for extracting a specific piece of information from the
+ * certificate and converting it into a {@link Principal}.
+ */
+public interface CertPrincipalResolver {
+  /**
+   * Resolves a {@link Principal} from the given X509 certificate.
+   *
+   * <p>This method is intended to extract principal information, such as a common name (CN) or an
+   * email address, from the specified certificate and encapsulate it into a {@link Principal}
+   * object. The specific field or attribute of the certificate to be used as the principal, and the
+   * logic for its extraction, is defined by the implementation.
+   *
+   * @param certificate The X509Certificate from which to resolve the principal.
+   * @return A {@link Principal} object representing the resolved principal from the certificate.
+   * @throws SSLPeerUnverifiedException If the peer's identity has not been verified.
+   * @throws CertificateParsingException If an error occurs while parsing the certificate for
+   *     principal information.
+   */
+  Principal resolvePrincipal(X509Certificate certificate)
+      throws SSLPeerUnverifiedException, CertificateParsingException;
+}
diff --git a/solr/core/src/java/org/apache/solr/security/cert/CertResolverPattern.java b/solr/core/src/java/org/apache/solr/security/cert/CertResolverPattern.java
new file mode 100644
index 00000000000..802743a516d
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/cert/CertResolverPattern.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.security.cert;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Represents a pattern for resolving certificate information, specifying the criteria for
+ * extracting and matching values from certificates.
+ */
+public class CertResolverPattern {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final String name;
+  private final String path;
+  private final CheckType checkType;
+  private final Set<String> filterValues;
+
+  /**
+   * Constructs a CertResolverPattern with specified parameters.
+   *
+   * @param name The name associated with this pattern.
+   * @param path The certificate field path this pattern applies to.
+   * @param checkType The type of check to perform on extracted values.
+   * @param filterValues The set of values to check against the extracted certificate field.
+   */
+  public CertResolverPattern(String name, String path, String checkType, Set<String> filterValues) {
+    this.name = name;
+    this.path = path;
+    this.checkType = CheckType.fromString(checkType);
+    this.filterValues = filterValues;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public String getPath() {
+    return path;
+  }
+
+  public CheckType getCheckType() {
+    return checkType;
+  }
+
+  public Set<String> getFilterValues() {
+    return filterValues;
+  }
+
+  public static boolean matchesPattern(String value, CertResolverPattern pattern) {
+    return matchesPattern(value, pattern.getCheckType(), pattern.getFilterValues());
+  }
+
+  /**
+   * Determines if a given value matches the specified filter values, depending on the check type.
+   *
+   * @param value The value to check.
+   * @param checkType The type of check to perform.
+   * @param values The set of values to check against.
+   * @return True if the value matches the criteria; false otherwise.
+   */
+  public static boolean matchesPattern(String value, CheckType checkType, Set<String> values) {
+    log.debug("matchesPattern   value:{}   checkType:{}    values:{}", value, checkType, values);
+    String lowerValue =
+        value.toLowerCase(Locale.ROOT); // lowercase for case-insensitive comparisons
+    switch (checkType) {
+      case EQUALS:
+        return values.contains(lowerValue);
+      case STARTS_WITH:
+        return values.stream().anyMatch(lowerValue::startsWith);
+      case ENDS_WITH:
+        return values.stream().anyMatch(lowerValue::endsWith);
+      case CONTAINS:
+        return values.stream().anyMatch(lowerValue::contains);
+      case WILDCARD:
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  /** Enum defining the types of checks that can be performed on extracted certificate values. */
+  public enum CheckType {
+    STARTS_WITH,
+    ENDS_WITH,
+    CONTAINS,
+    EQUALS,
+    WILDCARD;
+    // TODO: add regex support
+
+    private static final Map<String, CheckType> lookup =
+        Map.of(
+            "equals", EQUALS,
+            "startswith", STARTS_WITH,
+            "endswith", ENDS_WITH,
+            "contains", CONTAINS,
+            "*", WILDCARD,
+            "wildcard", WILDCARD);
+
+    public static CheckType fromString(String checkType) {
+      if (checkType == null) {
+        throw new IllegalArgumentException("CheckType cannot be null");
+      }
+      CheckType result = lookup.get(checkType.toLowerCase(Locale.ROOT));
+      if (result == null) {
+        throw new IllegalArgumentException("No CheckType with text '" + checkType + "' found");
+      }
+      return result;
+    }
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/security/cert/CertUtil.java b/solr/core/src/java/org/apache/solr/security/cert/CertUtil.java
new file mode 100644
index 00000000000..5a4e06bbe61
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/cert/CertUtil.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.security.cert;
+
+import java.lang.invoke.MethodHandles;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import javax.security.auth.x500.X500Principal;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class for certificate-related operations, including extracting fields from the subject or
+ * issuer DN and SAN fields from X509 certificates.
+ */
+public class CertUtil {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  public static final String SUBJECT_DN_PREFIX = "subject.dn";
+  public static final String ISSUER_DN_PREFIX = "issuer.dn";
+  public static final String SAN_PREFIX = "san.";
+
+  /**
+   * Extracts a specified field or the entire DN from an X500Principal, such as a certificate's
+   * subject or issuer. If the entire DN is returned the format would be RFC2253
+   *
+   * @param principal The X500Principal from which to extract information.
+   * @param path The DN field to extract, or a prefix indicating the entire DN.
+   * @return The value of the specified field, or the entire DN if just a prefix is provided.
+   */
+  public static Optional<String> extractFieldFromX500Principal(
+      X500Principal principal, String path) {
+    if (principal == null || path == null || path.isEmpty()) {
+      return Optional.empty();
+    }
+
+    String dn = principal.getName(X500Principal.RFC2253);
+
+    try {
+      final LdapName ln = new LdapName(dn);
+      // Path does not specify a field, return the whole DN after normalizing it
+      if (path.equalsIgnoreCase(SUBJECT_DN_PREFIX) || path.equalsIgnoreCase(ISSUER_DN_PREFIX)) {
+        // Remove whitespaces around RDNs, sort them, and reconstruct the DN string
+        List<String> rdns =
+            ln.getRdns().stream()
+                .map(rdn -> rdn.getType().trim() + "=" + rdn.getValue().toString().trim())
+                .sorted()
+                .collect(Collectors.toList());
+        dn = String.join(",", rdns);
+        return Optional.of(dn);
+      }
+
+      // Extract and return the specified DN field value
+      String field = null;
+      if (path.startsWith(SUBJECT_DN_PREFIX + ".")) {
+        field = path.substring((SUBJECT_DN_PREFIX + ".").length());
+      } else if (path.startsWith(ISSUER_DN_PREFIX + ".")) {
+        field = path.substring((ISSUER_DN_PREFIX + ".").length());
+      }
+
+      if (field != null) {
+        String fieldF = field;
+        return ln.getRdns().stream()
+            .filter(rdn -> rdn.getType().equalsIgnoreCase(fieldF))
+            .findFirst()
+            .map(Rdn::getValue)
+            .map(Object::toString);
+      }
+    } catch (InvalidNameException e) {
+      log.warn("Invalid DN in LdapName instantiation. DN={}", dn);
+    }
+    return Optional.empty();
+  }
+
+  /**
+   * Extracts a specified field or the entire subject DN from an X509 certificate.
+   *
+   * @param certificate The certificate from which to extract the subject DN information.
+   * @param path The path specifying the subject DN field to extract or a prefix for the entire DN.
+   * @return An Optional containing the value of the specified subject DN field or the entire DN;
+   *     empty if not found.
+   */
+  public static Optional<String> extractFromSubjectDN(X509Certificate certificate, String path) {
+    return extractFieldFromX500Principal(certificate.getSubjectX500Principal(), path);
+  }
+
+  /**
+   * Extracts a specified field or the entire issuer DN from an X509 certificate.
+   *
+   * @param certificate The certificate from which to extract the issuer DN information.
+   * @param path The path specifying the issuer DN field to extract or a prefix for the entire DN.
+   * @return An Optional containing the value of the specified issuer DN field or the entire DN;
+   *     empty if not found.
+   */
+  public static Optional<String> extractFromIssuerDN(X509Certificate certificate, String path) {
+    return extractFieldFromX500Principal(certificate.getIssuerX500Principal(), path);
+  }
+
+  /**
+   * Extracts SAN (Subject Alternative Name) fields from an X509 certificate that match a specified
+   * path and predicate.
+   *
+   * @param certificate The certificate from which to extract SAN information.
+   * @param path The path specifying the SAN field to extract.
+   * @param valueMatcher A predicate to apply to each SAN value for filtering.
+   * @return An Optional containing a list of SAN values that match the specified path and
+   *     predicate; empty if none found.
+   * @throws CertificateParsingException If an error occurs while parsing the certificate for SAN
+   *     fields.
+   */
+  public static Optional<List<String>> extractFromSAN(
+      X509Certificate certificate, String path, Predicate<String> valueMatcher)
+      throws CertificateParsingException {
+    Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
+    if (altNames == null) {
+      return Optional.empty();
+    }
+    List<String> filteredSANValues =
+        altNames.stream()
+            .filter(entry -> entry != null && entry.size() >= 2)
+            .filter(
+                entry -> {
+                  SANType sanType = SANType.fromValue((Integer) entry.get(0));
+                  return sanType != null && sanType.pathLowerCase().equals(path);
+                })
+            .map(entry -> (String) entry.get(1))
+            .filter(valueMatcher)
+            .collect(Collectors.toList());
+
+    return Optional.of(filteredSANValues);
+  }
+
+  /**
+   * Supported SAN (Subject Alternative Name) types as defined in <a
+   * href="https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6">RFC 5280</a>
+   */
+  public enum SANType {
+    OTHER_NAME(0), // OtherName
+    EMAIL(1), // rfc822Name
+    DNS(2), // dNSName
+    X400_ADDRESS(3), // x400Address
+    DIRECTORY_NAME(4), // directoryName
+    EDI_PARTY_NAME(5), // ediPartyName
+    URI(6), // uniformResourceIdentifier
+    IP_ADDRESS(7), // iPAddress
+    REGISTERED_ID(8); // registeredID
+
+    private static final Map<Integer, SANType> lookup =
+        EnumSet.allOf(SANType.class).stream()
+            .collect(Collectors.toMap(SANType::getValue, sanType -> sanType));
+
+    private final int value;
+
+    SANType(int value) {
+      this.value = value;
+    }
+
+    public int getValue() {
+      return value;
+    }
+
+    public static SANType fromValue(int value) {
+      return lookup.get(value);
+    }
+
+    public String path() {
+      return "SAN." + name();
+    }
+
+    public String pathLowerCase() {
+      return path().toLowerCase(Locale.ROOT);
+    }
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/security/cert/PathBasedCertPrincipalResolver.java b/solr/core/src/java/org/apache/solr/security/cert/PathBasedCertPrincipalResolver.java
new file mode 100644
index 00000000000..24640c0685e
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/cert/PathBasedCertPrincipalResolver.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.security.cert;
+
+import java.lang.invoke.MethodHandles;
+import java.security.Principal;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import org.apache.http.auth.BasicUserPrincipal;
+import org.apache.solr.common.SolrException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implements a {@link CertPrincipalResolver} that resolves a {@link Principal} from an X509
+ * certificate based on configurable paths, filters, and optional extraction patterns. This resolver
+ * can extract principal information from various certificate fields, such as Subject DN or SAN
+ * fields, according to the specified path. Additionally, it can further refine the extracted value
+ * based on optional "after" and "before" patterns, allowing for more precise control over the
+ * principal value.
+ *
+ * <p>Example configuration without extraction pattern:
+ *
+ * <pre>{@code
+ * "principalResolver": {
+ *   "class":"solr.PathBasedCertPrincipalResolver",
+ *   "params": {
+ *     "path":"SAN.email",
+ *     "filter":{
+ *       "checkType":"startsWith",
+ *       "values":["user@example"]
+ *     }
+ *   }
+ * }
+ * }</pre>
+ *
+ * In this configuration, the resolver is directed to extract email addresses from the SAN (Subject
+ * Alternative Name) field of the certificate and use them as principal names if they match the
+ * specified filter criteria.
+ *
+ * <p>Example configuration with extraction pattern:
+ *
+ * <pre>{@code
+ * "principalResolver": {
+ *   "class":"solr.PathBasedCertPrincipalResolver",
+ *   "params": {
+ *     "path":"SAN.email",
+ *     "filter":{
+ *       "checkType":"startsWith",
+ *       "values":["email_user1@example"]
+ *     },
+ *     "extract": {
+ *       "after":"_",
+ *       "before":"@"
+ *     }
+ *   }
+ * }
+ * }</pre>
+ *
+ * In this extended configuration, after extracting email addresses that match the filter criteria,
+ * the resolver further processes the extracted value to include only the portion after the "_"
+ * symbol and before "@". This allows for extracting specific parts of the principal value,
+ * providing additional flexibility and control.
+ */
+public class PathBasedCertPrincipalResolver extends PathBasedCertResolverBase
+    implements CertPrincipalResolver {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static final String PARAM_EXTRACT = "extract";
+  private static final String PARAM_AFTER = "after";
+  private static final String PARAM_BEFORE = "before";
+
+  private final CertResolverPattern pattern;
+  private final String startPattern;
+  private final String endPattern;
+
+  /**
+   * Constructs a new PathBasedCertPrincipalResolver with the specified configuration parameters.
+   *
+   * @param params The configuration parameters specifying the path and filter for extracting
+   *     principal information from certificates.
+   */
+  @SuppressWarnings("unchecked")
+  public PathBasedCertPrincipalResolver(Map<String, Object> params) {
+    this.pattern = createCertResolverPattern(params, CertUtil.SUBJECT_DN_PREFIX);
+    Map<String, String> extractConfig =
+        (Map<String, String>) params.getOrDefault(PARAM_EXTRACT, Collections.emptyMap());
+    this.startPattern = extractConfig.getOrDefault(PARAM_AFTER, "");
+    this.endPattern = extractConfig.getOrDefault(PARAM_BEFORE, "");
+  }
+
+  /**
+   * Resolves the principal from the given X509 certificate based on the configured path and filter.
+   * The first matching value, if any, is used as the principal name.
+   *
+   * @param certificate The X509Certificate from which to resolve the principal.
+   * @return A {@link Principal} object representing the resolved principal from the certificate.
+   * @throws SSLPeerUnverifiedException If the SSL peer is not verified.
+   * @throws CertificateParsingException If parsing the certificate fails.
+   */
+  @Override
+  public Principal resolvePrincipal(X509Certificate certificate)
+      throws SSLPeerUnverifiedException, CertificateParsingException {
+    Map<String, Set<String>> matches =
+        getValuesFromPaths(certificate, Collections.singletonList(pattern));
+    String basePrincipal = null;
+    if (matches != null && !matches.isEmpty()) {
+      Set<String> fieldValues = matches.getOrDefault(pattern.getName(), Collections.emptySet());
+      basePrincipal = fieldValues.stream().findFirst().orElse(null);
+    }
+
+    log.debug("Resolved basePrincipal: {}", basePrincipal);
+    if (basePrincipal == null) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "Resolved principal is null. "
+              + "No principal information was found matching the configuration");
+    }
+
+    String principal = extractPrincipal(basePrincipal);
+    log.debug("Resolved principal: {}", principal);
+    return new BasicUserPrincipal(principal);
+  }
+
+  private String extractPrincipal(String str) {
+    int start =
+        startPattern.isEmpty() || !str.contains(startPattern)
+            ? 0
+            : str.indexOf(startPattern) + startPattern.length();
+    int end = endPattern.isEmpty() ? str.length() : str.indexOf(endPattern, start);
+    if (start >= 0 && end > start && end <= str.length()) {
+      str = str.substring(start, end);
+    }
+    return str;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/security/cert/PathBasedCertResolverBase.java b/solr/core/src/java/org/apache/solr/security/cert/PathBasedCertResolverBase.java
new file mode 100644
index 00000000000..486a1193b8b
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/cert/PathBasedCertResolverBase.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.security.cert;
+
+import static org.apache.solr.security.cert.CertResolverPattern.matchesPattern;
+import static org.apache.solr.security.cert.CertUtil.ISSUER_DN_PREFIX;
+import static org.apache.solr.security.cert.CertUtil.SAN_PREFIX;
+import static org.apache.solr.security.cert.CertUtil.SUBJECT_DN_PREFIX;
+
+import java.lang.invoke.MethodHandles;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract base class for implementing path-based certificate resolvers. This class provides common
+ * functionality for extracting and processing certificate information based on configurable paths
+ * and filters.
+ */
+public abstract class PathBasedCertResolverBase {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static final String PARAM_NAME = "name";
+  private static final String PARAM_PATH = "path";
+  private static final String PARAM_FILTER = "filter";
+  private static final String PARAM_FILTER_CHECK_TYPE = "checkType";
+  private static final String PARAM_FILTER_VALUES = "values";
+
+  /**
+   * Creates a {@link CertResolverPattern} from a given configuration map and a default path. The
+   * pattern defines how certificate information should be extracted and processed.
+   *
+   * @param config The configuration map containing resolver settings.
+   * @param defaultPath The default path to use if none is specified in the configuration.
+   * @return A new instance of {@link CertResolverPattern} based on the provided configuration.
+   */
+  @SuppressWarnings("unchecked")
+  protected CertResolverPattern createCertResolverPattern(
+      Map<String, Object> config, String defaultPath) {
+    String path = ((String) config.getOrDefault(PARAM_PATH, defaultPath)).toLowerCase(Locale.ROOT);
+    String name = ((String) config.getOrDefault(PARAM_NAME, path)).toLowerCase(Locale.ROOT);
+    Map<String, Object> filter =
+        (Map<String, Object>) config.getOrDefault(PARAM_FILTER, Collections.emptyMap());
+    String checkType =
+        (String)
+            filter.getOrDefault(
+                PARAM_FILTER_CHECK_TYPE, CertResolverPattern.CheckType.WILDCARD.toString());
+    List<String> values =
+        (List<String>) filter.getOrDefault(PARAM_FILTER_VALUES, Collections.emptyList());
+    Set<String> lowerCaseValues =
+        values.stream().map(value -> value.toLowerCase(Locale.ROOT)).collect(Collectors.toSet());
+    return new CertResolverPattern(name, path, checkType, lowerCaseValues);
+  }
+
+  /**
+   * Extracts values from specified paths in a certificate and organizes them into a map. The map's
+   * keys are derived from the names specified in the patterns, and the values are sets of strings
+   * extracted from the certificate according to the pattern definitions.
+   *
+   * @param certificate The X509Certificate from which to extract information.
+   * @param patterns A list of {@link CertResolverPattern} objects defining the extraction criteria.
+   * @return A map where each key is a pattern name and each value is a set of extracted strings.
+   * @throws CertificateParsingException If an error occurs while parsing the certificate.
+   */
+  protected Map<String, Set<String>> getValuesFromPaths(
+      X509Certificate certificate, List<CertResolverPattern> patterns)
+      throws CertificateParsingException {
+    Map<String, Set<String>> fieldValuesMap = new HashMap<>();
+    for (CertResolverPattern pattern : patterns) {
+      String path = pattern.getPath();
+      if (path.startsWith(SUBJECT_DN_PREFIX) || path.startsWith(ISSUER_DN_PREFIX)) {
+        Optional<String> value =
+            path.startsWith(SUBJECT_DN_PREFIX)
+                ? CertUtil.extractFromSubjectDN(certificate, path)
+                : CertUtil.extractFromIssuerDN(certificate, path);
+        value.ifPresent(
+            val ->
+                fieldValuesMap
+                    .computeIfAbsent(pattern.getName(), k -> new LinkedHashSet<>())
+                    .add(val));
+      } else if (path.startsWith(SAN_PREFIX)) {
+        Optional<List<String>> sanValues =
+            CertUtil.extractFromSAN(certificate, path, value -> matchesPattern(value, pattern));
+        sanValues.ifPresent(
+            values ->
+                fieldValuesMap
+                    .computeIfAbsent(pattern.getName(), k -> new LinkedHashSet<>())
+                    .addAll(values));
+      } else {
+        throw new IllegalArgumentException(
+            "Invalid path in the certificate resolver pattern: " + path);
+      }
+    }
+    log.debug("Extracted field values: {}", fieldValuesMap);
+    return fieldValuesMap;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/security/cert/package-info.java b/solr/core/src/java/org/apache/solr/security/cert/package-info.java
new file mode 100644
index 00000000000..ab4eed56c0f
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/cert/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Certificate based authentication classes */
+package org.apache.solr.security.cert;
diff --git a/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java b/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java
index c961b1775b8..b42ce94dcd1 100644
--- a/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java
+++ b/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java
@@ -24,6 +24,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.security.cert.X509Certificate;
+import java.util.Collections;
 import javax.security.auth.x500.X500Principal;
 import javax.servlet.FilterChain;
 import javax.servlet.http.HttpServletRequest;
@@ -46,6 +47,7 @@ public class CertAuthPluginTest extends SolrTestCaseJ4 {
   public void setUp() throws Exception {
     super.setUp();
     plugin = new CertAuthPlugin();
+    plugin.init(Collections.emptyMap());
   }
 
   @Test
diff --git a/solr/core/src/test/org/apache/solr/security/PathBasedCertPrincipalResolverTest.java b/solr/core/src/test/org/apache/solr/security/PathBasedCertPrincipalResolverTest.java
new file mode 100644
index 00000000000..1911ff66d6b
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/security/PathBasedCertPrincipalResolverTest.java
@@ -0,0 +1,392 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.security;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.carrotsearch.randomizedtesting.RandomizedRunner;
+import java.security.Principal;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.security.auth.x500.X500Principal;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.security.cert.CertResolverPattern;
+import org.apache.solr.security.cert.CertUtil.SANType;
+import org.apache.solr.security.cert.PathBasedCertPrincipalResolver;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(RandomizedRunner.class)
+public class PathBasedCertPrincipalResolverTest extends SolrTestCaseJ4 {
+
+  private static final String SUBJECT_DN =
+      " CN=John Doe, O=Solr Corp, OU= Engineering, C=US , ST =California, L =San Francisco"; // whitespaces should be ignored
+  private static final String ISSUER_DN =
+      "CN = Issuer, O= Issuer Corp, OU =IT, C=US , ST=California, L =San Francisco ";
+
+  private X509Certificate mockCertificate;
+
+  @BeforeClass
+  public static void setupMockito() {
+    SolrTestCaseJ4.assumeWorkingMockito();
+  }
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    mockCertificate = mock(X509Certificate.class);
+  }
+
+  @Test
+  public void testSubjectDn() throws SSLPeerUnverifiedException, CertificateParsingException {
+    // CN
+    testCertificateCases(SUBJECT_DN, "subject.dn.CN", "John Doe");
+
+    // O
+    testCertificateCases(SUBJECT_DN, "subject.dn.O", "Solr Corp");
+
+    // OU
+    testCertificateCases(SUBJECT_DN, "subject.dn.OU", "Engineering");
+
+    // C
+    testCertificateCases(SUBJECT_DN, "subject.dn.C", "US");
+
+    // ST
+    testCertificateCases(SUBJECT_DN, "subject.dn.ST", "California");
+
+    // L
+    testCertificateCases(SUBJECT_DN, "subject.dn.L", "San Francisco");
+  }
+
+  @Test
+  public void testIssuerDn() throws SSLPeerUnverifiedException, CertificateParsingException {
+    // CN
+    testCertificateCases(ISSUER_DN, "issuer.dn.CN", "Issuer");
+
+    // O
+    testCertificateCases(ISSUER_DN, "issuer.dn.O", "Issuer Corp");
+
+    // OU
+    testCertificateCases(ISSUER_DN, "issuer.dn.OU", "IT");
+
+    // C
+    testCertificateCases(ISSUER_DN, "issuer.dn.C", "US");
+
+    // ST
+    testCertificateCases(ISSUER_DN, "issuer.dn.ST", "California");
+
+    // L
+    testCertificateCases(ISSUER_DN, "issuer.dn.L", "San Francisco");
+  }
+
+  @Test
+  public void testSan() {
+    // Email
+    testCertificateCases(
+        List.of(
+            List.of(SANType.EMAIL.getValue(), "user1@example.com"),
+            List.of(SANType.EMAIL.getValue(), "user2@example.com")),
+        "san.email",
+        "user1@example.com");
+
+    // Email with 'extract'
+    testCertificateCases(
+        List.of(List.of(SANType.EMAIL.getValue(), "user@example.com")),
+        null,
+        "san.email",
+        List.of("user@example.com"),
+        "startsWith",
+        Map.of("after", "@", "before", ".com"),
+        "example");
+    testCertificateCases(
+        List.of(List.of(SANType.EMAIL.getValue(), "user@example.com")),
+        null,
+        "san.email",
+        List.of("user@example.com"),
+        "equals",
+        Map.of("before", "@"),
+        "user");
+
+    // DNS
+    testCertificateCases(
+        List.of(
+            List.of(SANType.DNS.getValue(), "value1.example.com"),
+            List.of(SANType.DNS.getValue(), "value2.example.com")),
+        "san.dns",
+        "value1.example.com");
+
+    // URI
+    testCertificateCases(
+        List.of(
+            List.of(SANType.URI.getValue(), "http://example.com"),
+            List.of(SANType.URI.getValue(), "http://example2.org")),
+        "san.uri",
+        List.of("http://example.com"),
+        "equals",
+        "http://example.com");
+
+    // IP Address
+    testCertificateCases(
+        List.of(
+            List.of(SANType.IP_ADDRESS.getValue(), "192.168.1.1"),
+            List.of(SANType.IP_ADDRESS.getValue(), "10.0.0.1")),
+        "san.IP_ADDRESS",
+        "192.168.1.1");
+
+    // OTHER_NAME
+    testCertificateCases(
+        List.of(List.of(SANType.OTHER_NAME.getValue(), "1.2.3.4")), "san.OTHER_NAME", "1.2.3.4");
+
+    // X400_ADDRESS
+    testCertificateCases(
+        List.of(List.of(SANType.X400_ADDRESS.getValue(), "X400AddressValue")),
+        "san.X400_ADDRESS",
+        "X400AddressValue");
+
+    // DIRECTORY_NAME
+    testCertificateCases(
+        List.of(List.of(SANType.DIRECTORY_NAME.getValue(), "DirectoryNameValue")),
+        "san.DIRECTORY_NAME",
+        "DirectoryNameValue");
+
+    // EDI_PARTY_NAME
+    testCertificateCases(
+        List.of(List.of(SANType.EDI_PARTY_NAME.getValue(), "EdiPartyNameValue")),
+        "san.EDI_PARTY_NAME",
+        "EdiPartyNameValue");
+
+    // REGISTERED_ID
+    testCertificateCases(
+        List.of(List.of(SANType.REGISTERED_ID.getValue(), "RegisteredIdValue")),
+        "san.REGISTERED_ID",
+        "RegisteredIdValue");
+
+    //  CheckType tests
+    testCertificateCases(
+        List.of(
+            List.of(SANType.EMAIL.getValue(), "user1@example.com"),
+            List.of(SANType.EMAIL.getValue(), "user2@example.com")),
+        "san.email",
+        List.of("user2@example.com"),
+        "equals",
+        "user2@example.com");
+
+    testCertificateCases(
+        List.of(
+            List.of(SANType.EMAIL.getValue(), "user1@example.com"),
+            List.of(SANType.EMAIL.getValue(), "user2@example.com")),
+        "san.email",
+        List.of("user2"),
+        "contains",
+        "user2@example.com");
+
+    testCertificateCases(
+        List.of(
+            List.of(SANType.EMAIL.getValue(), "user1@example.com"),
+            List.of(SANType.EMAIL.getValue(), "user2@example.com")),
+        "san.email",
+        List.of("user2"),
+        "startsWith",
+        "user2@example.com");
+
+    testCertificateCases(
+        List.of(
+            List.of(SANType.EMAIL.getValue(), "user@example1.com"),
+            List.of(SANType.EMAIL.getValue(), "user@example2.com")),
+        "san.email",
+        List.of("example2.com"),
+        "endsWith",
+        "user@example2.com");
+
+    testCertificateCases(
+        List.of(
+            List.of(SANType.EMAIL.getValue(), "user1@example.com"),
+            List.of(SANType.EMAIL.getValue(), "user2@example.com")),
+        "san.email",
+        Collections.emptyList(),
+        "*",
+        "user1@example.com");
+  }
+
+  @Test
+  public void testMultipleSANEntries()
+      throws SSLPeerUnverifiedException, CertificateParsingException {
+
+    when(mockCertificate.getSubjectAlternativeNames())
+        .thenReturn(
+            List.of(
+                List.of(SANType.EMAIL.getValue(), "user1@example.com"),
+                List.of(SANType.EMAIL.getValue(), "user2@example.com")));
+
+    Map<String, Object> params =
+        Map.of(
+            "path",
+            "SAN.email",
+            "filter",
+            Map.of(
+                "checkType",
+                "equals",
+                "values",
+                List.of("user1@example.com", "user2@example.com")));
+
+    PathBasedCertPrincipalResolver resolver = new PathBasedCertPrincipalResolver(params);
+    Principal result = resolver.resolvePrincipal(mockCertificate);
+
+    assertNotNull(result);
+    assertTrue(
+        result.getName().contains("user1@example.com")
+            || result.getName().contains("user2@example.com"));
+  }
+
+  @Test
+  public void testResolverWithInvalidPath() {
+    Map<String, Object> params =
+        Map.of(
+            "path",
+            "Invalid.path",
+            "filter",
+            Map.of("checkType", "equals", "values", List.of("value1", "value2")));
+
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> new PathBasedCertPrincipalResolver(params).resolvePrincipal(mockCertificate));
+  }
+
+  @Test
+  public void testNoMatchFound() {
+    Map<String, Object> params =
+        Map.of(
+            "path",
+            "subject.dn.CN",
+            "filter",
+            Map.of("checkType", "equals", "values", List.of("NonExistent")));
+
+    PathBasedCertPrincipalResolver resolver = new PathBasedCertPrincipalResolver(params);
+    assertThrows(SolrException.class, () -> resolver.resolvePrincipal(mockCertificate));
+  }
+
+  @Test
+  public void testResolverWithExtractPatternMissing()
+      throws SSLPeerUnverifiedException, CertificateParsingException {
+    when(mockCertificate.getSubjectAlternativeNames())
+        .thenReturn(List.of(List.of(SANType.EMAIL.getValue(), "info@example.com")));
+
+    // Both 'after' and 'before' patterns are missing
+    final Map<String, Object> params = new HashMap<>();
+    params.put("path", "SAN.email");
+    params.put("filter", Map.of("checkType", "startsWith", "values", List.of("info@")));
+    assertResolvedPrincipal(params, "info@example.com");
+
+    // Only 'before' pattern is provided, 'after' is missing
+    params.put("extract", Map.of("before", ".com"));
+    assertResolvedPrincipal(params, "info@example");
+
+    // Only 'after' pattern is provided, 'before' is missing
+    params.put("extract", Map.of("after", "@"));
+    assertResolvedPrincipal(params, "example.com");
+
+    // Invalid 'after' pattern that doesn't exist in the SAN value
+    params.put("extract", Map.of("after", "notfound"));
+    assertResolvedPrincipal(
+        params,
+        "info@example.com"); // Expect the original value since the 'after' pattern was not found
+  }
+
+  private void testCertificateCases(List<List<?>> sanData, String path, String expectedValue) {
+
+    testCertificateCases(
+        sanData,
+        null,
+        path,
+        Collections.emptyList(),
+        CertResolverPattern.CheckType.WILDCARD.toString(),
+        Collections.emptyMap(),
+        expectedValue);
+  }
+
+  private void testCertificateCases(
+      List<List<?>> sanData,
+      String path,
+      List<String> filterValues,
+      String filterCheckType,
+      String expectedValue) {
+
+    testCertificateCases(
+        sanData, null, path, filterValues, filterCheckType, Collections.emptyMap(), expectedValue);
+  }
+
+  private void testCertificateCases(String dn, String path, String expectedValue) {
+
+    testCertificateCases(
+        null,
+        dn,
+        path,
+        Collections.emptyList(),
+        CertResolverPattern.CheckType.WILDCARD.toString(),
+        Collections.emptyMap(),
+        expectedValue);
+  }
+
+  private void testCertificateCases(
+      List<List<?>> sanData,
+      String dn,
+      String path,
+      List<String> filterValues,
+      String filterCheckType,
+      Map<String, String> extractPatterns,
+      String expectedValue) {
+    try {
+      if (path.startsWith("san")) {
+        when(mockCertificate.getSubjectAlternativeNames()).thenReturn(sanData);
+      } else if (path.startsWith("subject.dn")) {
+        X500Principal subjectDN = new X500Principal(dn);
+        when(mockCertificate.getSubjectX500Principal()).thenReturn(subjectDN);
+      } else if (path.startsWith("issuer.dn")) {
+        X500Principal issuerDN = new X500Principal(dn);
+        when(mockCertificate.getIssuerX500Principal()).thenReturn(issuerDN);
+      }
+
+      Map<String, Object> params = new HashMap<>();
+      params.put("path", path);
+      params.put("filter", Map.of("checkType", filterCheckType, "values", filterValues));
+      params.put("extract", extractPatterns);
+
+      assertResolvedPrincipal(params, expectedValue);
+    } catch (CertificateParsingException | SSLPeerUnverifiedException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void assertResolvedPrincipal(Map<String, Object> params, String expectedPrincipalName)
+      throws SSLPeerUnverifiedException, CertificateParsingException {
+    PathBasedCertPrincipalResolver resolver = new PathBasedCertPrincipalResolver(params);
+    Principal result = resolver.resolvePrincipal(mockCertificate);
+
+    assertNotNull(result);
+    assertEquals(expectedPrincipalName, result.getName());
+  }
+}
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cert-authentication-plugin.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cert-authentication-plugin.adoc
index 681388e21bf..318091a6106 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cert-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cert-authentication-plugin.adoc
@@ -40,10 +40,21 @@ These checks are described in the xref:enabling-ssl.adoc[] section.
 
 This plugin provides no additional checking beyond what has been configured via SSL properties.
 
-=== User Principal Extraction
+It is best practice to verify the actual contents of certificates issued by your trusted certificate authority before configuring authorization based on the contents.
+
+== User Principal Extraction
+
+This plugin will configure the user `principal` for the request based on the contents of the client certificate. Solr provides three methods for extracting the `principal`:
+
+*Simple Resolution*: Uses the full X500 subject name from the client certificate. This is the default behavior.
+
+*Path Based Resolution*: Offers greater flexibility by allowing to extract specific fields or patterns using the {solr-javadocs}/core/org/apache/solr/security/cert/PathBasedCertPrincipalResolver.html[PathBasedCertPrincipalResolver]. Here "`paths`" refers to the fields ("`.`" separated) defined in the certificate.
+
+*Custom Resolution*: If the built-in options do not meet your needs, you can add a custom class by implementing the {solr-javadocs}/core/org/apache/solr/security/cert/CertPrincipalResolver.html[`CertPrincipalResolver`]interface. This allows you to define your own logic for extracting and processing the `principal`. The custom implementation can then be referenced in your `security.json` configuration.
 
-This plugin will configure the user principal for the request based on the X500 subject present in the client certificate.
-Authorization plugins will need to accept and handle the full subject name, for example:
+=== Simple Principal Resolution
+
+By default, Solr uses the full subject name as the `principal`. For example:
 
 [source,text]
 ----
@@ -53,7 +64,377 @@ CN=Solr User,OU=Engineering,O=Example Inc.,C=US
 A list of possible tags that can be present in the subject name is available in https://tools.ietf.org/html/rfc5280#section-4.1.2.4[RFC-5280, Section 4.1.2.4].
 Values may have spaces, punctuation, and other characters.
 
-It is best practice to verify the actual contents of certificates issued by your trusted certificate authority before configuring authorization based on the contents.
+=== Configurable Principal Resolution
+
+Using the `PathBasedCertPrincipalResolver`, you can extract specific fields or apply filters to resolve the `principal`. For example:
+
+[source,json]
+----
+
+{
+  "authentication": {
+    "class": "solr.CertAuthPlugin",
+    "principalResolver": {
+      "class": "solr.PathBasedCertPrincipalResolver",
+      "params": {
+        "path": "subject.dn"
+      }
+    }
+  }
+}
+
+----
+
+This configuration extracts the entire Subject DN, e.g., CN=John Doe,O=Solr Corp,OU=Engineering,C=US.
+In fact, this is equivalent to the default `principalResolver` which is used if no `principalResolver` configuration is provided.
+
+You can also extract a specific field from the Subject DN:
+
+[source,json]
+----
+
+{
+  "authentication": {
+    "class": "solr.CertAuthPlugin",
+    "principalResolver": {
+      "class": "solr.PathBasedCertPrincipalResolver",
+      "params": {
+        "path": "subject.dn.CN"
+      }
+    }
+  }
+}
+
+----
+Behavior: This configuration extracts the Common Name (CN) field only, e.g., John Doe.
+
+The below configuration extracts the email address from the SAN field, e.g., user@example.com.
+
+[source,json]
+----
+
+{
+  "authentication": {
+    "class": "solr.CertAuthPlugin",
+    "principalResolver": {
+      "class": "solr.PathBasedCertPrincipalResolver",
+      "params": {
+        "path": "san.email"
+      }
+    }
+  }
+}
+
+----
+
+==== Filtered Extraction
+
+For more control, you can apply filters to extract `principals` that meet specific criteria.
+Filters use the `checkType` parameter to define matching rules and optionally support patterns for further refinement.
+When filtering matches multiple values, the first matching value is picked for the `principal`. The `values` filter represent a list of accepted values.
+
+==== Filtering with checkType
+
+The following example extracts email addresses from the `SAN` field only if they end with a specific domain:
+
+
+[source,json]
+----
+
+{
+  "authentication": {
+    "class": "solr.CertAuthPlugin",
+    "principalResolver": {
+      "class": "solr.PathBasedCertPrincipalResolver",
+      "params": {
+        "path": "san.email",
+        "filter": {
+          "checkType": "endsWith",
+          "values": ["@example.com"]
+        }
+      }
+    }
+  }
+}
+
+----
+
+The supported `checkType` options are:
+
+`equals`: Matches values exactly.
+
+`startsWith`: Matches values starting with the specified string.
+
+`endsWith`: Matches values ending with the specified string.
+
+`contains`: Matches values containing the specified string.
+
+`wildcard`: Matches any value.
+
+
+==== Combining Filters with Pattern Matching
+
+Filters can also use `extract` patterns (`after` and `before`) to refine the extracted `principal`.
+For example, given a cert with `SAN.email=eng_admin@example.com`, the following configuration resolves "admin" as the request's `principal`. This is done in 3 steps:
+
+- Read `SAN.email` fields.
+- Filter out to accept only emails ending with "@example.com" (pick the first match).
+- In the resolved email extract the string between "_" and "@".
+
+[source,json]
+----
+"principalResolver": {
+  "class": "solr.PathBasedCertPrincipalResolver",
+  "params": {
+    "path": "SAN.email",
+    "filter": {
+      "checkType": "endsWith",
+      "values": ["@example.com"]
+    },
+    "extract": {
+                 "after": "_",
+                 "before": "@",
+         }
+  }
+}
+
+----
+
+
+==== Match DNS Names Starting with a Prefix
+
+Behavior: Extracts `DNS` names like `service-api.example.com` or `service-db.example.org`:
+
+[source,json]
+----
+
+{
+  "authentication": {
+    "class": "solr.CertAuthPlugin",
+    "principalResolver": {
+      "class": "solr.PathBasedCertPrincipalResolver",
+      "params": {
+        "path": "san.dns",
+        "filter": {
+          "checkType": "startsWith",
+          "values": ["service-"]
+        }
+      }
+    }
+  }
+}
+
+----
+
+==== Match DNS Names Ending with a Specific Domain
+
+Behavior: Extract `DNS` names that end with a specific domain, such as `.example.com`:
+
+[source,json]
+----
+
+{
+  "authentication": {
+    "class": "solr.CertAuthPlugin",
+    "principalResolver": {
+      "class": "solr.PathBasedCertPrincipalResolver",
+      "params": {
+        "path": "san.dns",
+        "filter": {
+          "checkType": "endsWith",
+          "values": [".example.com"]
+        }
+      }
+    }
+  }
+}
+
+----
+
+==== Match Multiple Specific DNS Names
+
+Behavior: Extract `DNS` names if they match any value from a predefined list:
+
+[source,json]
+----
+
+{
+  "authentication": {
+    "class": "solr.CertAuthPlugin",
+    "principalResolver": {
+      "class": "solr.PathBasedCertPrincipalResolver",
+      "params": {
+        "path": "san.dns",
+        "filter": {
+          "checkType": "equals",
+          "values": ["api.example.com", "db.example.com"]
+        }
+      }
+    }
+  }
+}
+
+----
+
+
+==== Combine Filtering and Patterns
+
+Extract a portion of a `DNS` name that ends with .`example.com` but only return the portion before `.example.com`:
+
+
+[source,json]
+----
+
+{
+  "authentication": {
+    "class": "solr.CertAuthPlugin",
+    "principalResolver": {
+      "class": "solr.PathBasedCertPrincipalResolver",
+      "params": {
+        "path": "san.dns",
+        "filter": {
+          "checkType": "endsWith",
+          "values": [".example.com"]
+        },
+        "extract": {
+          "before": ".example.com"
+        }
+      }
+    }
+  }
+}
+
+----
+`Behavior`: For a DNS name like `service.example.com`, extracts only `service`.
+
+
+==== Filter and Extract with Both Prefix and Suffix
+
+Extract a portion of a DNS name that starts with `service-` and ends with `.example.com`:
+
+[source,json]
+----
+
+{
+  "authentication": {
+    "class": "solr.CertAuthPlugin",
+    "principalResolver": {
+      "class": "solr.PathBasedCertPrincipalResolver",
+      "params": {
+        "path": "san.dns",
+        "filter": {
+          "checkType": "startsWith",
+          "values": ["service-"]
+        },
+        "extract": {
+          "after": "service-",
+          "before": ".example.com"
+        }
+      }
+    }
+  }
+}
+
+----
+
+`Behavior`: For `service-api.example.com`, extracts only `api`.
+
+
+==== Summary of supported Fields
+
+==== Subject DN Fields:
+
+The list of supported fields is available in https://tools.ietf.org/html/rfc5280#section-4.1.2.4[RFC-5280, Section 4.1.2.4]. Below are most common fields:
+
+`subject.dn.CN` (Common Name)
+
+`subject.dn.O` (Organization)
+
+`subject.dn.OU` (Organizational Unit)
+
+`subject.dn.C` (Country)
+
+`subject.dn.ST` (State/Province)
+
+`subject.dn.L` (Locality)
+
+
+==== Issuer DN Fields:
+
+Same fields as Subject DN but prefixed with issuer.dn.
+
+==== SAN Fields:
+
+List of supported SAN fields is compatible with https://tools.ietf.org/html/rfc5280#section-4.2.1.6[RFC-5280, Section 4.2.1.6].
+
+`san.email` (Email addresses)
+
+`san.dns` (DNS names)
+
+`san.uri` (URIs)
+
+`san.ipaddress` (IP Addresses)
+
+`san.othername` (Other names)
+
+`san.x400address` (X400 addresses)
+
+`san.directoryname` (Directory names)
+
+`san.edipartyname` (EDI party names)
+
+`san.registeredid` (Registered IDs)
+
+
+=== Custom Principal Resolution
+
+To use a custom `principal` resolver, implement the {solr-javadocs}/core/org/apache/solr/security/cert/CertPrincipalResolver.html[`CertPrincipalResolver`]  interface in your class. The interface requires you to define how to resolve a principal from an `X.509` certificate. Here's an example of a basic custom implementation:
+
+[source,java]
+----
+package com.example.solr;
+
+import java.security.Principal;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import org.apache.solr.security.cert.CertPrincipalResolver;
+import org.apache.solr.security.cert.CertPrincipalResolver;
+
+public class CustomCertPrincipalResolver implements CertPrincipalResolver {
+
+  public CustomCertPrincipalResolver(Map<String, Object> params) {
+    // use the 'params' object for some initialization
+  }
+
+  @Override
+  public Principal resolvePrincipal(X509Certificate certificate) throws SSLPeerUnverifiedException {
+    // Custom logic to extract the principal
+    String customPrincipalName = certificate.getSubjectX500Principal().getName();
+    // Modify or process the principal name if needed
+    return new BasicUserPrincipal(customPrincipalName);
+  }
+}
+
+----
+
+*Using the Custom Resolver in security.json*
+
+Once the custom resolver class is implemented and available in your Solr classpath, reference it in the `security.json` configuration file:
+
+[source,json]
+----
+
+{
+  "authentication": {
+    "class": "solr.CertAuthPlugin",
+    "principalResolver": {
+      "class": "com.example.solr.CustomCertPrincipalResolver",
+      "params": {}
+    }
+  }
+}
+
+----
 
 == Using Certificate Auth with Clients (including SolrJ)