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

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


The following commit(s) were added to refs/heads/main by this push:
     new e7b6b0ca7b [ #9767]feat(oauth): Support Regex to have user mapping 
from principal (#9788)
e7b6b0ca7b is described below

commit e7b6b0ca7b05902237e41fd861772e9eb20dc8d2
Author: Bharath Krishna <[email protected]>
AuthorDate: Thu Jan 29 22:38:19 2026 -0800

    [ #9767]feat(oauth): Support Regex to have user mapping from principal 
(#9788)
    
    ### What changes were proposed in this pull request?
    - Core framework: PrincipalMapper interface, RegexPrincipalMapper
    implementation, PrincipalMapperFactory
    - Kerberos utility: KerberosPrincipalParser for parsing
    primary[/instance][@REALM] format
    - Authenticator integration: Modified KerberosAuthenticator,
    JwksTokenValidator, and StaticSignKeyValidator to apply principal
    mapping
    - Configuration: Added 4 new properties (2 for OAuth, 2 for Kerberos)
    supporting regex patterns or custom mapper classes
    - Documentation: Added principal mapping section with OAuth/Kerberos
    examples and custom mapper implementation guide
    
    ### Why are the changes needed?
    
    Organizations need to transform authenticated principals into different
    user identities for authorization:
    
    OAuth: Extract username from email ([email protected] → user)
    Kerberos: Extract primary from service principals (HTTP/server@REALM →
    HTTP)
    Multi-realm: Route by realm ([email protected] → dev_user)
    
    Fixes #9767
    
    
    
    ### Does this PR introduce _any_ user-facing change?
    
    New configuration properties (all optional, defaults maintain backward
    compatibility):
    
    ```
    gravitino.authenticator.oauth.principalMapperType (default: regex)
    gravitino.authenticator.oauth.principalMapperPattern (default: ^(.*)$)
    gravitino.authenticator.kerberos.principalMapperType (default: regex)
    gravitino.authenticator.kerberos.principalMapperPattern (default: ([^@]+).*)
    ```
    
    Backward compatible: Because the default values preserve existing
    behavior.
    
    ### How was this patch tested?
    
    - New test cases across 3 test classes: TestRegexPrincipalMapper,
    TestKerberosPrincipalParser, TestPrincipalMapperFactory
    - Existing tests: All OAuth and Kerberos authenticator tests pass
---
 .../apache/gravitino/auth/KerberosPrincipal.java   | 162 ++++++++++++++
 .../gravitino/auth/KerberosPrincipalMapper.java    |  86 ++++++++
 .../org/apache/gravitino/auth/PrincipalMapper.java |  40 ++++
 .../gravitino/auth/PrincipalMapperFactory.java     |  56 +++++
 .../gravitino/auth/RegexPrincipalMapper.java       | 100 +++++++++
 .../gravitino/auth/TestKerberosPrincipal.java      | 122 +++++++++++
 .../auth/TestKerberosPrincipalMapper.java          | 174 ++++++++++++++++
 .../gravitino/auth/TestPrincipalMapperFactory.java |  83 ++++++++
 .../gravitino/auth/TestRegexPrincipalMapper.java   | 140 +++++++++++++
 docs/security/how-to-authenticate.md               |  79 +++++++
 .../server/authentication/JwksTokenValidator.java  |  12 +-
 .../authentication/KerberosAuthenticator.java      |  35 ++--
 .../server/authentication/KerberosConfig.java      |  22 ++
 .../server/authentication/OAuthConfig.java         |  22 ++
 .../authentication/StaticSignKeyValidator.java     |  35 +++-
 .../authentication/TestJwksTokenValidator.java     | 159 ++++++++++++++
 .../authentication/TestStaticSignKeyValidator.java | 232 +++++++++++++++++++++
 17 files changed, 1536 insertions(+), 23 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/auth/KerberosPrincipal.java 
b/core/src/main/java/org/apache/gravitino/auth/KerberosPrincipal.java
new file mode 100644
index 0000000000..c4c1099789
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/auth/KerberosPrincipal.java
@@ -0,0 +1,162 @@
+/*
+ * 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.gravitino.auth;
+
+import java.security.Principal;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Represents a Kerberos principal with its structured components.
+ *
+ * <p>A Kerberos principal has the format: {@code primary[/instance][@REALM]} 
where:
+ *
+ * <ul>
+ *   <li><b>primary</b> (required): The primary component, typically the user 
or service name
+ *   <li><b>instance</b> (optional): The secondary component, often a hostname 
for service
+ *       principals
+ *   <li><b>realm</b> (optional): The Kerberos realm (domain), typically 
uppercase
+ * </ul>
+ *
+ * <p>Examples:
+ *
+ * <ul>
+ *   <li>{@code john} - primary only
+ *   <li>{@code [email protected]} - primary with realm
+ *   <li>{@code HTTP/[email protected]} - service principal with 
instance and realm
+ * </ul>
+ */
+public class KerberosPrincipal implements Principal {
+
+  private final String username;
+  private final String instance;
+  private final String realm;
+  private final String fullPrincipal;
+
+  /**
+   * Creates a new Kerberos principal.
+   *
+   * @param username the primary username component (required, cannot be null 
or empty)
+   * @param instance the instance component (optional, can be null)
+   * @param realm the realm component (optional, can be null)
+   * @throws IllegalArgumentException if username is null or empty
+   */
+  public KerberosPrincipal(String username, String instance, String realm) {
+    if (username == null || username.isEmpty()) {
+      throw new IllegalArgumentException("Username cannot be null or empty");
+    }
+    this.username = username;
+    this.instance = instance;
+    this.realm = realm;
+    this.fullPrincipal = buildFullPrincipal(username, instance, realm);
+  }
+
+  /**
+   * Gets the primary component (username). This is the name used for 
authentication.
+   *
+   * @return the username (never null or empty)
+   */
+  @Override
+  public String getName() {
+    return username;
+  }
+
+  /**
+   * Gets the instance (secondary component) of this principal, if present.
+   *
+   * @return an Optional containing the instance, or empty if no instance
+   */
+  public Optional<String> getInstance() {
+    return Optional.ofNullable(instance);
+  }
+
+  /**
+   * Gets the realm of this principal, if present.
+   *
+   * @return an Optional containing the realm, or empty if no realm
+   */
+  public Optional<String> getRealm() {
+    return Optional.ofNullable(realm);
+  }
+
+  /**
+   * Gets the full principal string in Kerberos format.
+   *
+   * @return the full principal string (e.g., "user/instance@REALM")
+   */
+  public String getFullPrincipal() {
+    return fullPrincipal;
+  }
+
+  /**
+   * Gets the primary component with instance (if present), without realm.
+   *
+   * @return primary/instance (e.g., "HTTP/server") or just primary if no 
instance
+   */
+  public String getPrimaryWithInstance() {
+    if (instance != null && !instance.isEmpty()) {
+      return username + "/" + instance;
+    }
+    return username;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof KerberosPrincipal)) {
+      return false;
+    }
+    KerberosPrincipal that = (KerberosPrincipal) o;
+    return Objects.equals(username, that.username)
+        && Objects.equals(instance, that.instance)
+        && Objects.equals(realm, that.realm);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(username, instance, realm);
+  }
+
+  @Override
+  public String toString() {
+    return "KerberosPrincipal{" + "fullPrincipal='" + fullPrincipal + '\'' + 
'}';
+  }
+
+  /**
+   * Builds the full principal string from components.
+   *
+   * @param username the username component
+   * @param instance the instance component (optional)
+   * @param realm the realm component (optional)
+   * @return the full principal string
+   */
+  private static String buildFullPrincipal(String username, String instance, 
String realm) {
+    StringBuilder sb = new StringBuilder(username);
+    if (instance != null && !instance.isEmpty()) {
+      sb.append('/').append(instance);
+    }
+    if (realm != null && !realm.isEmpty()) {
+      sb.append('@').append(realm);
+    }
+    return sb.toString();
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/auth/KerberosPrincipalMapper.java 
b/core/src/main/java/org/apache/gravitino/auth/KerberosPrincipalMapper.java
new file mode 100644
index 0000000000..32748a28c9
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/auth/KerberosPrincipalMapper.java
@@ -0,0 +1,86 @@
+/*
+ * 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.gravitino.auth;
+
+import java.security.Principal;
+
+/**
+ * Kerberos principal mapper that parses Kerberos principal strings into 
{@link KerberosPrincipal}
+ * objects.
+ *
+ * <p>This mapper is thread-safe and can be shared across multiple 
authentication requests.
+ */
+public class KerberosPrincipalMapper implements PrincipalMapper {
+
+  /**
+   * Maps a Kerberos principal string to a KerberosPrincipal object.
+   *
+   * @param principal the Kerberos principal string to parse (e.g., 
"user/instance@REALM")
+   * @return a KerberosPrincipal object containing the parsed components
+   * @throws IllegalArgumentException if the principal string is null, empty, 
or has invalid format
+   */
+  @Override
+  public Principal map(String principal) {
+    if (principal == null || principal.isEmpty()) {
+      throw new IllegalArgumentException("Principal string cannot be null or 
empty");
+    }
+
+    // Kerberos principal format: user[/instance][@REALM]
+    // Find positions of '/' and '@'
+    int slashIndex = principal.indexOf('/');
+    int atIndex = principal.indexOf('@');
+
+    String username;
+    String instance = null;
+    String realm = null;
+
+    // Extract username (up to '/' or '@', whichever comes first)
+    if (slashIndex >= 0 && atIndex >= 0) {
+      // Both '/' and '@' present
+      if (slashIndex < atIndex) {
+        // Format: user/instance@REALM
+        username = principal.substring(0, slashIndex);
+        instance = principal.substring(slashIndex + 1, atIndex);
+        realm = principal.substring(atIndex + 1);
+      } else {
+        // Invalid format: '@' before '/'
+        throw new IllegalArgumentException("Invalid Kerberos principal format: 
" + principal);
+      }
+    } else if (slashIndex >= 0) {
+      // Only '/' present: user/instance
+      username = principal.substring(0, slashIndex);
+      instance = principal.substring(slashIndex + 1);
+    } else if (atIndex >= 0) {
+      // Only '@' present: user@REALM
+      username = principal.substring(0, atIndex);
+      realm = principal.substring(atIndex + 1);
+    } else {
+      // No delimiters: just username
+      username = principal;
+    }
+
+    // Validate username is not empty
+    if (username.isEmpty()) {
+      throw new IllegalArgumentException("Username cannot be empty in 
principal: " + principal);
+    }
+
+    return new KerberosPrincipal(username, instance, realm);
+  }
+}
diff --git a/core/src/main/java/org/apache/gravitino/auth/PrincipalMapper.java 
b/core/src/main/java/org/apache/gravitino/auth/PrincipalMapper.java
new file mode 100644
index 0000000000..f4af0e6c3d
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/auth/PrincipalMapper.java
@@ -0,0 +1,40 @@
+/*
+ * 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.gravitino.auth;
+
+import java.security.Principal;
+
+/**
+ * Interface for mapping authenticated principals to user identities.
+ *
+ * <p>Implementations should be thread-safe as they may be shared across 
multiple authentication
+ * requests.
+ */
+public interface PrincipalMapper {
+
+  /**
+   * Maps a principal string to a Principal object.
+   *
+   * @param principal the principal string to map (e.g., "[email protected]", 
"user/instance@REALM")
+   * @return a Principal object containing the mapped username
+   * @throws IllegalArgumentException if the principal cannot be mapped
+   */
+  Principal map(String principal);
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/auth/PrincipalMapperFactory.java 
b/core/src/main/java/org/apache/gravitino/auth/PrincipalMapperFactory.java
new file mode 100644
index 0000000000..01439fd020
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/auth/PrincipalMapperFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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.gravitino.auth;
+
+/** Factory class for creating {@link PrincipalMapper} instances. */
+public class PrincipalMapperFactory {
+
+  private PrincipalMapperFactory() {}
+
+  /**
+   * Creates a principal mapper based on the type and configuration.
+   *
+   * @param mapperType "regex" for built-in regex mapper, or fully qualified 
class name for custom
+   *     mapper
+   * @param regexPattern the regex pattern (only used when mapperType is 
"regex")
+   * @return a configured PrincipalMapper instance
+   * @throws IllegalArgumentException if the mapper cannot be created
+   */
+  public static PrincipalMapper create(String mapperType, String regexPattern) 
{
+    if ("regex".equalsIgnoreCase(mapperType)) {
+      return new RegexPrincipalMapper(regexPattern);
+    }
+
+    // Load custom mapper class
+    try {
+      Class<?> clazz = Class.forName(mapperType);
+      if (!PrincipalMapper.class.isAssignableFrom(clazz)) {
+        throw new IllegalArgumentException(
+            "Class " + mapperType + " does not implement PrincipalMapper");
+      }
+      return (PrincipalMapper) clazz.getDeclaredConstructor().newInstance();
+    } catch (ClassNotFoundException e) {
+      throw new IllegalArgumentException("Unknown principal mapper type: " + 
mapperType, e);
+    } catch (Exception e) {
+      throw new IllegalArgumentException(
+          "Failed to instantiate principal mapper: " + mapperType, e);
+    }
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/auth/RegexPrincipalMapper.java 
b/core/src/main/java/org/apache/gravitino/auth/RegexPrincipalMapper.java
new file mode 100644
index 0000000000..a72d4e39ff
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/auth/RegexPrincipalMapper.java
@@ -0,0 +1,100 @@
+/*
+ * 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.gravitino.auth;
+
+import java.security.Principal;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.gravitino.UserPrincipal;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Regex-based principal mapper that extracts username using regex patterns 
with capturing groups.
+ *
+ * <p>This implementation is thread-safe as Pattern.matcher() creates 
thread-local Matcher
+ * instances.
+ */
+public class RegexPrincipalMapper implements PrincipalMapper {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(RegexPrincipalMapper.class);
+
+  private final Pattern pattern;
+
+  /**
+   * Creates a new regex principal mapper.
+   *
+   * @param patternStr the regex pattern with a capturing group (required)
+   * @throws IllegalArgumentException if the pattern string has invalid regex 
syntax
+   */
+  public RegexPrincipalMapper(String patternStr) {
+    if (patternStr == null || patternStr.isEmpty()) {
+      throw new IllegalArgumentException("Pattern string cannot be null or 
empty");
+    }
+    this.pattern = Pattern.compile(patternStr);
+    LOG.info("Initialized RegexPrincipalMapper with pattern: {}", patternStr);
+  }
+
+  /**
+   * Maps a principal string to a Principal using the configured regex pattern.
+   *
+   * @param principal the principal string to map
+   * @return a Principal containing the extracted username from the first 
capturing group, or the
+   *     original principal if no match
+   * @throws IllegalArgumentException if pattern matching fails
+   */
+  @Override
+  public Principal map(String principal) {
+    if (principal == null) {
+      return null;
+    }
+
+    try {
+      Matcher matcher = pattern.matcher(principal);
+
+      // If pattern matches and has at least one capturing group, return the 
first group
+      if (matcher.find() && matcher.groupCount() >= 1) {
+        String extracted = matcher.group(1);
+        // Return extracted value if it's not null and not empty, otherwise 
original principal
+        String username = (extracted != null && !extracted.isEmpty()) ? 
extracted : principal;
+        return new UserPrincipal(username);
+      }
+
+      // If pattern doesn't match or has no capturing groups, return original 
principal
+      return new UserPrincipal(principal);
+
+    } catch (Exception e) {
+      String message =
+          String.format(
+              "Error applying regex pattern '%s' to principal '%s'", 
pattern.pattern(), principal);
+      LOG.error("{}: {}", message, e.getMessage());
+      throw new IllegalArgumentException(message, e);
+    }
+  }
+
+  /**
+   * Gets the configured regex pattern string.
+   *
+   * @return the regex pattern string
+   */
+  public String getPatternString() {
+    return pattern.pattern();
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/auth/TestKerberosPrincipal.java 
b/core/src/test/java/org/apache/gravitino/auth/TestKerberosPrincipal.java
new file mode 100644
index 0000000000..99fa0add2f
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/auth/TestKerberosPrincipal.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.gravitino.auth;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+public class TestKerberosPrincipal {
+
+  @Test
+  public void testBasicConstruction() {
+    KerberosPrincipal principal = new KerberosPrincipal("john", "admin", 
"EXAMPLE.COM");
+
+    assertEquals("john", principal.getName());
+    assertEquals("john/admin", principal.getPrimaryWithInstance());
+    assertTrue(principal.getInstance().isPresent());
+    assertEquals("admin", principal.getInstance().get());
+    assertTrue(principal.getRealm().isPresent());
+    assertEquals("EXAMPLE.COM", principal.getRealm().get());
+    assertEquals("john/[email protected]", principal.getFullPrincipal());
+  }
+
+  @Test
+  public void testPrincipalWithoutInstance() {
+    KerberosPrincipal principal = new KerberosPrincipal("john", null, 
"EXAMPLE.COM");
+
+    assertEquals("john", principal.getName());
+    assertEquals("john", principal.getPrimaryWithInstance());
+    assertFalse(principal.getInstance().isPresent());
+    assertTrue(principal.getRealm().isPresent());
+    assertEquals("EXAMPLE.COM", principal.getRealm().get());
+    assertEquals("[email protected]", principal.getFullPrincipal());
+  }
+
+  @Test
+  public void testPrincipalWithoutRealm() {
+    KerberosPrincipal principal = new KerberosPrincipal("john", "admin", null);
+
+    assertEquals("john", principal.getName());
+    assertEquals("john/admin", principal.getPrimaryWithInstance());
+    assertTrue(principal.getInstance().isPresent());
+    assertEquals("admin", principal.getInstance().get());
+    assertFalse(principal.getRealm().isPresent());
+    assertEquals("john/admin", principal.getFullPrincipal());
+  }
+
+  @Test
+  public void testMinimalPrincipal() {
+    KerberosPrincipal principal = new KerberosPrincipal("john", null, null);
+
+    assertEquals("john", principal.getName());
+    assertEquals("john", principal.getPrimaryWithInstance());
+    assertFalse(principal.getInstance().isPresent());
+    assertFalse(principal.getRealm().isPresent());
+    assertEquals("john", principal.getFullPrincipal());
+  }
+
+  @Test
+  public void testNullUsernameThrowsException() {
+    assertThrows(IllegalArgumentException.class, () -> new 
KerberosPrincipal(null, null, null));
+  }
+
+  @Test
+  public void testEmptyUsernameThrowsException() {
+    assertThrows(IllegalArgumentException.class, () -> new 
KerberosPrincipal("", null, null));
+  }
+
+  @Test
+  public void testEquality() {
+    KerberosPrincipal principal1 = new KerberosPrincipal("john", "admin", 
"EXAMPLE.COM");
+    KerberosPrincipal principal2 = new KerberosPrincipal("john", "admin", 
"EXAMPLE.COM");
+    KerberosPrincipal principal3 = new KerberosPrincipal("john", "other", 
"EXAMPLE.COM");
+
+    assertEquals(principal1, principal2);
+    assertEquals(principal1.hashCode(), principal2.hashCode());
+    assertFalse(principal1.equals(principal3));
+  }
+
+  @Test
+  public void testToString() {
+    KerberosPrincipal principal =
+        new KerberosPrincipal("HTTP", "server.example.com", "EXAMPLE.COM");
+    String toString = principal.toString();
+
+    assertNotNull(toString);
+    assertTrue(toString.contains("HTTP/[email protected]"));
+  }
+
+  @Test
+  public void testServicePrincipal() {
+    KerberosPrincipal principal =
+        new KerberosPrincipal("HTTP", "server.example.com", "EXAMPLE.COM");
+
+    assertEquals("HTTP", principal.getName());
+    assertEquals("HTTP/server.example.com", 
principal.getPrimaryWithInstance());
+    assertEquals("server.example.com", principal.getInstance().get());
+    assertEquals("EXAMPLE.COM", principal.getRealm().get());
+    assertEquals("HTTP/[email protected]", 
principal.getFullPrincipal());
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/auth/TestKerberosPrincipalMapper.java 
b/core/src/test/java/org/apache/gravitino/auth/TestKerberosPrincipalMapper.java
new file mode 100644
index 0000000000..334ec53f9c
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/auth/TestKerberosPrincipalMapper.java
@@ -0,0 +1,174 @@
+/*
+ * 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.gravitino.auth;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestKerberosPrincipalMapper {
+
+  private KerberosPrincipalMapper mapper;
+
+  @BeforeEach
+  public void setUp() {
+    mapper = new KerberosPrincipalMapper();
+  }
+
+  @Test
+  public void testUsernameOnly() {
+    KerberosPrincipal principal = (KerberosPrincipal) mapper.map("john");
+
+    assertEquals("john", principal.getName());
+    assertEquals("john", principal.getPrimaryWithInstance());
+    assertFalse(principal.getInstance().isPresent());
+    assertFalse(principal.getRealm().isPresent());
+    assertEquals("john", principal.getFullPrincipal());
+  }
+
+  @Test
+  public void testUsernameWithRealm() {
+    KerberosPrincipal principal = (KerberosPrincipal) 
mapper.map("[email protected]");
+
+    assertEquals("john", principal.getName());
+    assertEquals("john", principal.getPrimaryWithInstance());
+    assertFalse(principal.getInstance().isPresent());
+    assertTrue(principal.getRealm().isPresent());
+    assertEquals("EXAMPLE.COM", principal.getRealm().get());
+    assertEquals("[email protected]", principal.getFullPrincipal());
+  }
+
+  @Test
+  public void testServicePrincipalWithInstanceAndRealm() {
+    KerberosPrincipal principal =
+        (KerberosPrincipal) mapper.map("HTTP/[email protected]");
+
+    assertEquals("HTTP", principal.getName());
+    assertEquals("HTTP/server.example.com", 
principal.getPrimaryWithInstance());
+    assertTrue(principal.getInstance().isPresent());
+    assertEquals("server.example.com", principal.getInstance().get());
+    assertTrue(principal.getRealm().isPresent());
+    assertEquals("EXAMPLE.COM", principal.getRealm().get());
+    assertEquals("HTTP/[email protected]", 
principal.getFullPrincipal());
+  }
+
+  @Test
+  public void testUsernameWithInstance() {
+    KerberosPrincipal principal = (KerberosPrincipal) mapper.map("user/admin");
+
+    assertEquals("user", principal.getName());
+    assertEquals("user/admin", principal.getPrimaryWithInstance());
+    assertTrue(principal.getInstance().isPresent());
+    assertEquals("admin", principal.getInstance().get());
+    assertFalse(principal.getRealm().isPresent());
+    assertEquals("user/admin", principal.getFullPrincipal());
+  }
+
+  @Test
+  public void testNullPrincipal() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          mapper.map(null);
+        });
+  }
+
+  @Test
+  public void testEmptyPrincipal() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          mapper.map("");
+        });
+  }
+
+  @Test
+  public void testEmptyUsername() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          mapper.map("@EXAMPLE.COM");
+        });
+  }
+
+  @Test
+  public void testInvalidFormatRealmBeforeSlash() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          mapper.map("[email protected]/instance");
+        });
+  }
+
+  @Test
+  public void testConstructorWithNullUsername() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          new KerberosPrincipal(null, null, null);
+        });
+  }
+
+  @Test
+  public void testConstructorWithEmptyUsername() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          new KerberosPrincipal("", null, null);
+        });
+  }
+
+  @Test
+  public void testEqualsAndHashCode() {
+    KerberosPrincipal principal1 = new KerberosPrincipal("john", "admin", 
"EXAMPLE.COM");
+    KerberosPrincipal principal2 = new KerberosPrincipal("john", "admin", 
"EXAMPLE.COM");
+    KerberosPrincipal principal3 = new KerberosPrincipal("john", null, 
"EXAMPLE.COM");
+
+    assertEquals(principal1, principal2);
+    assertEquals(principal1.hashCode(), principal2.hashCode());
+    assertFalse(principal1.equals(principal3));
+  }
+
+  @Test
+  public void testToString() {
+    KerberosPrincipal principal =
+        (KerberosPrincipal) mapper.map("HTTP/[email protected]");
+    String toString = principal.toString();
+
+    assertNotNull(toString);
+    assertTrue(toString.contains("HTTP/[email protected]"));
+  }
+
+  @Test
+  public void testConstructorDirectly() {
+    KerberosPrincipal principal = new KerberosPrincipal("john", "admin", 
"EXAMPLE.COM");
+
+    assertEquals("john", principal.getName());
+    assertEquals("john/admin", principal.getPrimaryWithInstance());
+    assertEquals("admin", principal.getInstance().get());
+    assertEquals("EXAMPLE.COM", principal.getRealm().get());
+    assertEquals("john/[email protected]", principal.getFullPrincipal());
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/auth/TestPrincipalMapperFactory.java 
b/core/src/test/java/org/apache/gravitino/auth/TestPrincipalMapperFactory.java
new file mode 100644
index 0000000000..117e6167a8
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/auth/TestPrincipalMapperFactory.java
@@ -0,0 +1,83 @@
+/*
+ * 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.gravitino.auth;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.security.Principal;
+import org.apache.gravitino.UserPrincipal;
+import org.junit.jupiter.api.Test;
+
+public class TestPrincipalMapperFactory {
+
+  @Test
+  public void testCreateRegexMapper() {
+    PrincipalMapper mapper = PrincipalMapperFactory.create("regex", 
"([^@]+)@.*");
+
+    assertNotNull(mapper);
+    assertTrue(mapper instanceof RegexPrincipalMapper);
+
+    Principal principal = mapper.map("[email protected]");
+    assertEquals("user", principal.getName());
+  }
+
+  @Test
+  public void testCreateRegexMapperWithDefaultPattern() {
+    PrincipalMapper mapper = PrincipalMapperFactory.create("regex", "^(.*)$");
+
+    assertNotNull(mapper);
+    assertTrue(mapper instanceof RegexPrincipalMapper);
+
+    Principal principal = mapper.map("[email protected]");
+    assertTrue(principal instanceof UserPrincipal);
+    assertEquals("[email protected]", principal.getName());
+  }
+
+  @Test
+  public void testCreateCustomMapperWithInvalidClass() {
+    IllegalArgumentException exception =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> {
+              PrincipalMapperFactory.create("unknown.InvalidClass", null);
+            });
+    assertTrue(exception.getMessage().contains("Unknown principal mapper 
type"));
+  }
+
+  public static class TestCustomMapper implements PrincipalMapper {
+    @Override
+    public Principal map(String principal) {
+      return new UserPrincipal("custom:" + principal);
+    }
+  }
+
+  @Test
+  public void testCreateCustomMapperWithInlineClass() {
+    String className = TestCustomMapper.class.getName();
+    PrincipalMapper mapper = PrincipalMapperFactory.create(className, null);
+    assertNotNull(mapper);
+    assertTrue(mapper instanceof TestCustomMapper);
+    Principal principal = mapper.map("foo");
+    assertEquals("custom:foo", principal.getName());
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/auth/TestRegexPrincipalMapper.java 
b/core/src/test/java/org/apache/gravitino/auth/TestRegexPrincipalMapper.java
new file mode 100644
index 0000000000..d56dd94ceb
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/auth/TestRegexPrincipalMapper.java
@@ -0,0 +1,140 @@
+/*
+ * 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.gravitino.auth;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.security.Principal;
+import org.apache.gravitino.UserPrincipal;
+import org.junit.jupiter.api.Test;
+
+public class TestRegexPrincipalMapper {
+
+  @Test
+  public void testExtractUsernameFromEmail() {
+    RegexPrincipalMapper mapper = new RegexPrincipalMapper("([^@]+)@.*");
+    Principal principal = mapper.map("[email protected]");
+
+    assertNotNull(principal);
+    assertEquals("john.doe", principal.getName());
+    assertTrue(principal instanceof UserPrincipal);
+  }
+
+  @Test
+  public void testExtractUsernameFromKerberosPrincipal() {
+    RegexPrincipalMapper mapper = new RegexPrincipalMapper("([^/@]+).*");
+
+    // Test user@REALM format
+    Principal principal1 = mapper.map("[email protected]");
+    assertNotNull(principal1);
+    assertEquals("john", principal1.getName());
+
+    // Test user/instance@REALM format
+    Principal principal2 = mapper.map("HTTP/[email protected]");
+    assertNotNull(principal2);
+    assertEquals("HTTP", principal2.getName());
+  }
+
+  @Test
+  public void testNullPattern() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          new RegexPrincipalMapper(null);
+        });
+  }
+
+  @Test
+  public void testEmptyPattern() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          new RegexPrincipalMapper("");
+        });
+  }
+
+  @Test
+  public void testNullPrincipal() {
+    RegexPrincipalMapper mapper = new RegexPrincipalMapper("([^@]+)@.*");
+    Principal principal = mapper.map(null);
+
+    assertNull(principal);
+  }
+
+  @Test
+  public void testPatternWithNoCapturingGroup() {
+    RegexPrincipalMapper mapper = new RegexPrincipalMapper(".*@company.com");
+    Principal principal = mapper.map("[email protected]");
+
+    // If pattern matches but has no capturing group, return original
+    assertNotNull(principal);
+    assertEquals("[email protected]", principal.getName());
+  }
+
+  @Test
+  public void testPatternDoesNotMatch() {
+    RegexPrincipalMapper mapper = new 
RegexPrincipalMapper("([^@]+)@company.com");
+    Principal principal = mapper.map("[email protected]");
+
+    // If pattern doesn't match, return original
+    assertNotNull(principal);
+    assertEquals("[email protected]", principal.getName());
+  }
+
+  @Test
+  public void testInvalidRegexPattern() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          new RegexPrincipalMapper("[invalid(regex");
+        });
+  }
+
+  @Test
+  public void testEmptyCapturingGroup() {
+    RegexPrincipalMapper mapper = new RegexPrincipalMapper("().*");
+    Principal principal = mapper.map("[email protected]");
+
+    // If capturing group is empty, return original principal
+    assertNotNull(principal);
+    assertEquals("[email protected]", principal.getName());
+  }
+
+  @Test
+  public void testComplexPattern() {
+    // Extract username from Azure AD format: [email protected]
+    RegexPrincipalMapper mapper = new 
RegexPrincipalMapper("([^@]+)@[^.]+\\..*");
+    Principal principal = mapper.map("[email protected]");
+
+    assertNotNull(principal);
+    assertEquals("john.doe", principal.getName());
+  }
+
+  @Test
+  public void testGetPatternString() {
+    String pattern = "([^@]+)@.*";
+    RegexPrincipalMapper mapper = new RegexPrincipalMapper(pattern);
+    assertEquals(pattern, mapper.getPatternString());
+  }
+}
diff --git a/docs/security/how-to-authenticate.md 
b/docs/security/how-to-authenticate.md
index ef5338cb5f..b349f0b80e 100644
--- a/docs/security/how-to-authenticate.md
+++ b/docs/security/how-to-authenticate.md
@@ -113,6 +113,81 @@ GravitinoClient client = GravitinoClient.builder(uri)
     .build();
 ```
 
+### Principal mapping
+
+Gravitino supports principal mapping to transform authenticated principals 
(from OAuth or Kerberos) into user identities for authorization. By default, 
Gravitino uses regex-based mapping.
+
+#### OAuth principal mapping
+
+For OAuth authentication, principals are extracted from JWT claims (configured 
via `gravitino.authenticator.oauth.principalFields`). You can customize how 
these principals are mapped:
+
+```text
+# Use default regex mapper that extracts everything (passes through unchanged)
+gravitino.authenticator.oauth.principalMapper = regex
+gravitino.authenticator.oauth.principalMapper.regex.pattern = ^(.*)$
+
+# Extract username from email (e.g., [email protected] -> user)
+gravitino.authenticator.oauth.principalMapper = regex
+gravitino.authenticator.oauth.principalMapper.regex.pattern = ([^@]+)@.*
+
+# Use custom mapper implementation
+gravitino.authenticator.oauth.principalMapper = com.example.MyCustomMapper
+```
+
+#### Kerberos principal mapping
+
+For Kerberos authentication, principals follow the format 
`primary[/instance][@REALM]`. The default mapper extracts the primary component 
(username before `@`):
+
+```text
+# Default: Extract primary component (user@REALM -> user, HTTP/server@REALM -> 
HTTP)
+gravitino.authenticator.kerberos.principalMapper = regex
+gravitino.authenticator.kerberos.principalMapper.regex.pattern = ([^@]+).*
+
+# Extract only the first part before '/' (HTTP/server@REALM -> HTTP)
+gravitino.authenticator.kerberos.principalMapper = regex
+gravitino.authenticator.kerberos.principalMapper.regex.pattern = ([^/@]+).*
+```
+
+#### Custom principal mapper
+
+For advanced use cases, implement the `PrincipalMapper` interface:
+
+```java
+package com.example;
+
+import org.apache.gravitino.auth.KerberosPrincipal;
+import org.apache.gravitino.auth.KerberosPrincipalMapper;
+import org.apache.gravitino.auth.PrincipalMapper;
+
+import java.security.Principal;
+
+public class RealmBasedMapper implements PrincipalMapper {
+  private final KerberosPrincipalMapper parser = new KerberosPrincipalMapper();
+
+  @Override
+  public Principal map(String principal) {
+    // Parse Kerberos principal components
+    KerberosPrincipal krbPrincipal = (KerberosPrincipal) parser.map(principal);
+    
+    // Route based on realm
+    if ("DEV.EXAMPLE.COM".equals(krbPrincipal.getRealm().orElse(null))) {
+      return () -> "dev_" + krbPrincipal.getName();
+    } else if 
("PROD.EXAMPLE.COM".equals(krbPrincipal.getRealm().orElse(null))) {
+      return () -> "prod_" + krbPrincipal.getName();
+    }
+    
+    // Default: use primary with instance (e.g., "HTTP/server")
+    return () -> krbPrincipal.getPrimaryWithInstance();
+  }
+}
+```
+
+Configure Gravitino to use your custom mapper:
+
+```text
+gravitino.authenticator.kerberos.principalMapper = com.example.RealmBasedMapper
+```
+
 ### Server configuration
 
 Gravitino server and Gravitino Iceberg REST server share the same 
configuration items, you doesn't need to add `gravitino.iceberg-rest` prefix 
for Gravitino Iceberg REST server.
@@ -134,8 +209,12 @@ Gravitino server and Gravitino Iceberg REST server share 
the same configuration
 | `gravitino.authenticator.oauth.jwksUri`             | JWKS URI for 
server-side OAuth token validation. Required when using JWKS-based validation.  
                                                                                
                                                                                
           | (none)                                                             
 | Yes if `tokenValidatorClass` is 
`org.apache.gravitino.server.authentication.JwksTokenValidator` | 1. [...]
 | `gravitino.authenticator.oauth.principalFields`     | JWT claim field(s) to 
use as principal identity. Comma-separated list for fallback in order (e.g., 
'preferred_username,email,sub').                                                
                                                                                
     | `sub`                                                               | No 
                                                                                
             | 1. [...]
 | `gravitino.authenticator.oauth.tokenValidatorClass` | Fully qualified class 
name of the OAuth token validator implementation. Use 
`org.apache.gravitino.server.authentication.JwksTokenValidator` for JWKS-based 
validation or 
`org.apache.gravitino.server.authentication.StaticSignKeyValidator` for static 
key validation. | 
`org.apache.gravitino.server.authentication.StaticSignKeyValidator` | No        
                                                                                
      | 1. [...]
+| `gravitino.authenticator.oauth.principalMapper` | Principal mapper type for 
OAuth. Use 'regex' for regex-based mapping, or provide a fully qualified class 
name implementing `org.apache.gravitino.auth.PrincipalMapper`.                  
                                                                               
| `regex`                                                             | No      
                                                                                
        | 1.2.0  [...]
+| `gravitino.authenticator.oauth.principalMapper.regex.pattern` | Regex 
pattern for OAuth principal mapping. First capture group becomes the mapped 
principal. Only used when principalMapper is 'regex'.                           
                                                                                
                | `^(.*)$`                                                      
      | No                                                                      
                         [...]
 | `gravitino.authenticator.kerberos.principal`        | Indicates the Kerberos 
principal to be used for HTTP endpoint. Principal should start with `HTTP/`.    
                                                                                
                                                                                
 | (none)                                                              | Yes if 
use `kerberos` as the authenticator                                             
         | 0. [...]
 | `gravitino.authenticator.kerberos.keytab`           | Location of the keytab 
file with the credentials for the principal.                                    
                                                                                
                                                                                
 | (none)                                                              | Yes if 
use `kerberos` as the authenticator                                             
         | 0. [...]
+| `gravitino.authenticator.kerberos.principalMapper` | Principal mapper type 
for Kerberos. Use 'regex' for regex-based mapping, or provide a fully qualified 
class name implementing `org.apache.gravitino.auth.PrincipalMapper`.            
                                                                                
| `regex`                                                             | No      
                                                                                
        | 1.2.0 [...]
+| `gravitino.authenticator.kerberos.principalMapper.regex.pattern` | Regex 
pattern for Kerberos principal mapping. First capture group becomes the mapped 
principal. Only used when principalMapper is 'regex'.                           
                                                                                
            | `([^@]+).*`                                                       
  | No                                                                          
                   [...]
 
 The signature algorithms that Gravitino supports follows:
 
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/JwksTokenValidator.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/JwksTokenValidator.java
index 5bd0681d47..e358a8998d 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/JwksTokenValidator.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/JwksTokenValidator.java
@@ -36,7 +36,8 @@ import java.util.List;
 import java.util.Set;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Config;
-import org.apache.gravitino.UserPrincipal;
+import org.apache.gravitino.auth.PrincipalMapper;
+import org.apache.gravitino.auth.PrincipalMapperFactory;
 import org.apache.gravitino.exceptions.UnauthorizedException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -55,6 +56,7 @@ public class JwksTokenValidator implements 
OAuthTokenValidator {
   private String expectedIssuer;
   private List<String> principalFields;
   private long allowSkewSeconds;
+  private PrincipalMapper principalMapper;
 
   @Override
   public void initialize(Config config) {
@@ -63,6 +65,11 @@ public class JwksTokenValidator implements 
OAuthTokenValidator {
     this.principalFields = config.get(OAuthConfig.PRINCIPAL_FIELDS);
     this.allowSkewSeconds = config.get(OAuthConfig.ALLOW_SKEW_SECONDS);
 
+    // Create principal mapper based on configuration
+    String mapperType = config.get(OAuthConfig.PRINCIPAL_MAPPER);
+    String regexPattern = 
config.get(OAuthConfig.PRINCIPAL_MAPPER_REGEX_PATTERN);
+    this.principalMapper = PrincipalMapperFactory.create(mapperType, 
regexPattern);
+
     LOG.info("Initializing JWKS token validator");
 
     if (StringUtils.isBlank(jwksUri)) {
@@ -132,7 +139,8 @@ public class JwksTokenValidator implements 
OAuthTokenValidator {
         throw new UnauthorizedException("No valid principal found in token");
       }
 
-      return new UserPrincipal(principal);
+      // Use principal mapper to extract username
+      return principalMapper.map(principal);
 
     } catch (Exception e) {
       LOG.error("JWKS JWT validation error: {}", e.getMessage());
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/KerberosAuthenticator.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/KerberosAuthenticator.java
index c5aff415cb..a14f3b3abb 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/KerberosAuthenticator.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/KerberosAuthenticator.java
@@ -11,22 +11,21 @@
  */
 package org.apache.gravitino.server.authentication;
 
-import com.google.common.base.Splitter;
 import java.io.File;
 import java.nio.charset.StandardCharsets;
 import java.security.Principal;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
 import java.util.Base64;
-import java.util.List;
 import javax.security.auth.Subject;
 import javax.security.auth.kerberos.KerberosPrincipal;
 import javax.security.auth.kerberos.KeyTab;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Config;
-import org.apache.gravitino.UserPrincipal;
 import org.apache.gravitino.auth.AuthConstants;
 import org.apache.gravitino.auth.KerberosUtils;
+import org.apache.gravitino.auth.PrincipalMapper;
+import org.apache.gravitino.auth.PrincipalMapperFactory;
 import org.apache.gravitino.exceptions.UnauthorizedException;
 import org.ietf.jgss.GSSContext;
 import org.ietf.jgss.GSSCredential;
@@ -50,6 +49,7 @@ public class KerberosAuthenticator implements Authenticator {
   public static final Logger LOG = 
LoggerFactory.getLogger(KerberosAuthenticator.class);
   private final Subject serverSubject = new Subject();
   private GSSManager gssManager;
+  private PrincipalMapper principalMapper;
 
   @Override
   public void initialize(Config config) throws RuntimeException {
@@ -75,6 +75,13 @@ public class KerberosAuthenticator implements Authenticator {
       KeyTab keytabInstance = KeyTab.getInstance(keytabFile);
       serverSubject.getPrivateCredentials().add(keytabInstance);
 
+      // Initialize principal mapper. The default regex pattern '([^@]+).*'
+      // extracts everything before '@' (e.g., "user" from "user@REALM",
+      // "HTTP/host" from "HTTP/host@REALM")
+      String mapperType = config.get(KerberosConfig.PRINCIPAL_MAPPER);
+      String regexPattern = 
config.get(KerberosConfig.PRINCIPAL_MAPPER_REGEX_PATTERN);
+      this.principalMapper = PrincipalMapperFactory.create(mapperType, 
regexPattern);
+
       gssManager =
           Subject.doAs(
               serverSubject,
@@ -170,23 +177,13 @@ public class KerberosAuthenticator implements 
Authenticator {
         throw new UnauthorizedException("GssContext isn't established", 
challenge);
       }
 
-      // Usually principal names are in the form 'user/instance@REALM' or 
'user@REALM'.
-      List<String> principalComponents =
-          Splitter.on('@').splitToList(gssContext.getSrcName().toString());
-      if (principalComponents.size() != 2) {
-        throw new UnauthorizedException("Principal has wrong format", 
AuthConstants.NEGOTIATE);
-      }
+      // Extract principal from GSS context and map it using configured mapper
+      String principalString = gssContext.getSrcName().toString();
 
-      String user = principalComponents.get(0);
-      // TODO: We will have KerberosUserPrincipal in the future.
-      //  We can put more information of Kerberos to the KerberosUserPrincipal
-      // For example, we can put the token into the KerberosUserPrincipal,
-      // We can return the token to the client in the AuthenticationFilter. It 
will be convenient
-      // for client to establish the security context. Hadoop uses the cookie 
to store the token.
-      // For now, we don't store it in the cookie. I can have a simple 
implementation. first.
-      // It's also not required for the protocol.
-      // https://datatracker.ietf.org/doc/html/rfc4559
-      return new UserPrincipal(user);
+      // Use principal mapper to extract the principal
+      // This allows for flexible mapping strategies (regex, kerberos-specific 
parsing, etc.)
+      // The mapper will handle validation and extraction based on its 
configured type
+      return principalMapper.map(principalString);
     } finally {
       if (gssContext != null) {
         gssContext.dispose();
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/KerberosConfig.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/KerberosConfig.java
index bec463420d..ab0a76ddc7 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/KerberosConfig.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/KerberosConfig.java
@@ -42,4 +42,26 @@ public interface KerberosConfig {
           .stringConf()
           .checkValue(StringUtils::isNotBlank, 
ConfigConstants.NOT_BLANK_ERROR_MSG)
           .create();
+
+  ConfigEntry<String> PRINCIPAL_MAPPER =
+      new ConfigBuilder(KERBEROS_CONFIG_PREFIX + "principalMapper")
+          .doc(
+              "Type of principal mapper to use for Kerberos authentication. "
+                  + "Built-in value: 'regex' (uses regex pattern to extract 
username). "
+                  + "Can also be a fully qualified class name implementing 
PrincipalMapper for custom logic. "
+                  + "Custom mappers can use KerberosPrincipalMapper for 
structured Kerberos principal parsing.")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .createWithDefault("regex");
+
+  ConfigEntry<String> PRINCIPAL_MAPPER_REGEX_PATTERN =
+      new ConfigBuilder(KERBEROS_CONFIG_PREFIX + 
"principalMapper.regex.pattern")
+          .doc(
+              "Regex pattern to extract the username from the Kerberos 
principal. "
+                  + "Only used when principalMapper is 'regex'. "
+                  + "The pattern should contain at least one capturing group. "
+                  + "Default pattern '([^@]+).*' extracts everything before 
'@' (including instance if present).")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .createWithDefault("([^@]+).*");
 }
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/OAuthConfig.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/OAuthConfig.java
index c16b625960..3d3bfdbfa8 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/OAuthConfig.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/OAuthConfig.java
@@ -144,4 +144,26 @@ public interface OAuthConfig {
           .version(ConfigConstants.VERSION_1_0_0)
           .stringConf()
           
.createWithDefault("org.apache.gravitino.server.authentication.StaticSignKeyValidator");
+
+  ConfigEntry<String> PRINCIPAL_MAPPER =
+      new ConfigBuilder(OAUTH_CONFIG_PREFIX + "principalMapper")
+          .doc(
+              "Type of principal mapper to use for OAuth/JWT principals. "
+                  + "Built-in value: 'regex' (uses regex pattern to extract 
username). "
+                  + "Default pattern '^(.*)$' keeps the principal unchanged. "
+                  + "Can also be a fully qualified class name implementing 
PrincipalMapper for custom logic.")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .createWithDefault("regex");
+
+  ConfigEntry<String> PRINCIPAL_MAPPER_REGEX_PATTERN =
+      new ConfigBuilder(OAUTH_CONFIG_PREFIX + "principalMapper.regex.pattern")
+          .doc(
+              "Regex pattern to extract the username from the OAuth principal 
field. "
+                  + "Only used when principalMapper is 'regex'. "
+                  + "The pattern should contain at least one capturing group. "
+                  + "Default pattern '^(.*)$' matches the entire principal.")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .stringConf()
+          .createWithDefault("^(.*)$");
 }
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/StaticSignKeyValidator.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/StaticSignKeyValidator.java
index 0fd59b7353..a6e94693f6 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/StaticSignKeyValidator.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/StaticSignKeyValidator.java
@@ -38,7 +38,8 @@ import java.util.Base64;
 import java.util.List;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Config;
-import org.apache.gravitino.UserPrincipal;
+import org.apache.gravitino.auth.PrincipalMapper;
+import org.apache.gravitino.auth.PrincipalMapperFactory;
 import org.apache.gravitino.auth.SignatureAlgorithmFamilyType;
 import org.apache.gravitino.exceptions.UnauthorizedException;
 
@@ -52,6 +53,8 @@ import org.apache.gravitino.exceptions.UnauthorizedException;
 public class StaticSignKeyValidator implements OAuthTokenValidator {
   private long allowSkewSeconds;
   private Key defaultSigningKey;
+  private PrincipalMapper principalMapper;
+  private List<String> principalFields;
 
   @Override
   public void initialize(Config config) {
@@ -65,6 +68,11 @@ public class StaticSignKeyValidator implements 
OAuthTokenValidator {
         "The uri of the default OAuth server can't be blank");
     String algType = config.get(OAuthConfig.SIGNATURE_ALGORITHM_TYPE);
     this.defaultSigningKey = 
decodeSignKey(Base64.getDecoder().decode(configuredSignKey), algType);
+    this.principalFields = config.get(OAuthConfig.PRINCIPAL_FIELDS);
+    // Create principal mapper based on configuration
+    String mapperType = config.get(OAuthConfig.PRINCIPAL_MAPPER);
+    String regexPattern = 
config.get(OAuthConfig.PRINCIPAL_MAPPER_REGEX_PATTERN);
+    this.principalMapper = PrincipalMapperFactory.create(mapperType, 
regexPattern);
   }
 
   @Override
@@ -97,7 +105,12 @@ public class StaticSignKeyValidator implements 
OAuthTokenValidator {
         throw new UnauthorizedException(
             "Audiences in token is not in expected format: %s", 
audienceObject);
       }
-      return new UserPrincipal(jwt.getBody().getSubject());
+
+      // Extract principal from JWT claims using configured field(s)
+      String principal = extractPrincipal(jwt.getBody());
+
+      // Use principal mapper to extract username
+      return principalMapper.map(principal);
     } catch (ExpiredJwtException
         | UnsupportedJwtException
         | MalformedJwtException
@@ -107,6 +120,24 @@ public class StaticSignKeyValidator implements 
OAuthTokenValidator {
     }
   }
 
+  /** Extracts the principal from the JWT claims using configured field(s). */
+  private String extractPrincipal(Claims claims) {
+    // Try the principal field(s) one by one in order
+    if (principalFields != null && !principalFields.isEmpty()) {
+      for (String field : principalFields) {
+        if (StringUtils.isNotBlank(field)) {
+          Object claimValue = claims.get(field);
+          if (claimValue != null) {
+            return claimValue.toString();
+          }
+        }
+      }
+    }
+
+    throw new UnauthorizedException(
+        "No valid principal found in token. Checked fields: %s", 
principalFields);
+  }
+
   private static Key decodeSignKey(byte[] key, String algType) {
     try {
       SignatureAlgorithmFamilyType algFamilyType =
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
 
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
index a2f2362d87..3068b5aee5 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
@@ -389,4 +389,163 @@ public class TestJwksTokenValidator {
     // The message should not contain "not initialized" or similar
     assertTrue(exception.getMessage().toLowerCase().contains("jwt"));
   }
+
+  @Test
+  public void testPrincipalMapperWithDefaultPattern() throws Exception {
+    // Generate a test RSA key pair
+    RSAKey rsaKey =
+        new 
RSAKeyGenerator(2048).keyID("test-key-id").algorithm(JWSAlgorithm.RS256).generate();
+
+    // Create a signed JWT token with email as subject
+    JWTClaimsSet claimsSet =
+        new JWTClaimsSet.Builder()
+            .subject("[email protected]")
+            .audience("test-service")
+            .issuer("https://test-issuer.com";)
+            .expirationTime(Date.from(Instant.now().plusSeconds(3600)))
+            .issueTime(Date.from(Instant.now()))
+            .build();
+
+    SignedJWT signedJWT =
+        new SignedJWT(
+            new 
JWSHeader.Builder(JWSAlgorithm.RS256).keyID("test-key-id").build(), claimsSet);
+
+    JWSSigner signer = new RSASSASigner(rsaKey);
+    signedJWT.sign(signer);
+
+    String tokenString = signedJWT.serialize();
+
+    // Mock the JWKSourceBuilder to return our test key
+    try (MockedStatic<JWKSourceBuilder> mockedBuilder = 
mockStatic(JWKSourceBuilder.class)) {
+      @SuppressWarnings("unchecked")
+      JWKSource<SecurityContext> mockJwkSource = mock(JWKSource.class);
+      @SuppressWarnings("unchecked")
+      JWKSourceBuilder<SecurityContext> mockBuilder = 
mock(JWKSourceBuilder.class);
+
+      mockedBuilder.when(() -> 
JWKSourceBuilder.create(any(URL.class))).thenReturn(mockBuilder);
+      when(mockBuilder.build()).thenReturn(mockJwkSource);
+      when(mockJwkSource.get(any(), any())).thenReturn(Arrays.asList(rsaKey));
+
+      // Initialize validator with default user mapping pattern
+      Map<String, String> config = new HashMap<>();
+      config.put(
+          "gravitino.authenticator.oauth.jwksUri", 
"https://test-jwks.com/.well-known/jwks.json";);
+      config.put("gravitino.authenticator.oauth.authority", 
"https://test-issuer.com";);
+      config.put("gravitino.authenticator.oauth.principalMapper", "regex");
+      
config.put("gravitino.authenticator.oauth.principalMapper.regex.pattern", 
"(.*)");
+      config.put("gravitino.authenticator.oauth.allowSkewSecs", "120");
+
+      validator.initialize(createConfig(config));
+      Principal result = validator.validateToken(tokenString, "test-service");
+
+      assertNotNull(result);
+      assertEquals("[email protected]", result.getName());
+    }
+  }
+
+  @Test
+  public void testPrincipalMapperExtractEmailLocalPart() throws Exception {
+    // Generate a test RSA key pair
+    RSAKey rsaKey =
+        new 
RSAKeyGenerator(2048).keyID("test-key-id").algorithm(JWSAlgorithm.RS256).generate();
+
+    // Create a signed JWT token with email as subject
+    JWTClaimsSet claimsSet =
+        new JWTClaimsSet.Builder()
+            .subject("[email protected]")
+            .audience("test-service")
+            .issuer("https://test-issuer.com";)
+            .expirationTime(Date.from(Instant.now().plusSeconds(3600)))
+            .issueTime(Date.from(Instant.now()))
+            .build();
+
+    SignedJWT signedJWT =
+        new SignedJWT(
+            new 
JWSHeader.Builder(JWSAlgorithm.RS256).keyID("test-key-id").build(), claimsSet);
+
+    JWSSigner signer = new RSASSASigner(rsaKey);
+    signedJWT.sign(signer);
+
+    String tokenString = signedJWT.serialize();
+
+    // Mock the JWKSourceBuilder to return our test key
+    try (MockedStatic<JWKSourceBuilder> mockedBuilder = 
mockStatic(JWKSourceBuilder.class)) {
+      @SuppressWarnings("unchecked")
+      JWKSource<SecurityContext> mockJwkSource = mock(JWKSource.class);
+      @SuppressWarnings("unchecked")
+      JWKSourceBuilder<SecurityContext> mockBuilder = 
mock(JWKSourceBuilder.class);
+
+      mockedBuilder.when(() -> 
JWKSourceBuilder.create(any(URL.class))).thenReturn(mockBuilder);
+      when(mockBuilder.build()).thenReturn(mockJwkSource);
+      when(mockJwkSource.get(any(), any())).thenReturn(Arrays.asList(rsaKey));
+
+      // Initialize validator with pattern to extract email local part
+      Map<String, String> config = new HashMap<>();
+      config.put(
+          "gravitino.authenticator.oauth.jwksUri", 
"https://test-jwks.com/.well-known/jwks.json";);
+      config.put("gravitino.authenticator.oauth.authority", 
"https://test-issuer.com";);
+      config.put("gravitino.authenticator.oauth.principalMapper", "regex");
+      
config.put("gravitino.authenticator.oauth.principalMapper.regex.pattern", 
"([^@]+)@.*");
+      config.put("gravitino.authenticator.oauth.allowSkewSecs", "120");
+
+      validator.initialize(createConfig(config));
+      Principal result = validator.validateToken(tokenString, "test-service");
+
+      assertNotNull(result);
+      assertEquals("john.doe", result.getName());
+    }
+  }
+
+  @Test
+  public void testPrincipalMapperNoMatch() throws Exception {
+    // Generate a test RSA key pair
+    RSAKey rsaKey =
+        new 
RSAKeyGenerator(2048).keyID("test-key-id").algorithm(JWSAlgorithm.RS256).generate();
+
+    // Create a signed JWT token with simple username (no @)
+    JWTClaimsSet claimsSet =
+        new JWTClaimsSet.Builder()
+            .subject("plainuser")
+            .audience("test-service")
+            .issuer("https://test-issuer.com";)
+            .expirationTime(Date.from(Instant.now().plusSeconds(3600)))
+            .issueTime(Date.from(Instant.now()))
+            .build();
+
+    SignedJWT signedJWT =
+        new SignedJWT(
+            new 
JWSHeader.Builder(JWSAlgorithm.RS256).keyID("test-key-id").build(), claimsSet);
+
+    JWSSigner signer = new RSASSASigner(rsaKey);
+    signedJWT.sign(signer);
+
+    String tokenString = signedJWT.serialize();
+
+    // Mock the JWKSourceBuilder to return our test key
+    try (MockedStatic<JWKSourceBuilder> mockedBuilder = 
mockStatic(JWKSourceBuilder.class)) {
+      @SuppressWarnings("unchecked")
+      JWKSource<SecurityContext> mockJwkSource = mock(JWKSource.class);
+      @SuppressWarnings("unchecked")
+      JWKSourceBuilder<SecurityContext> mockBuilder = 
mock(JWKSourceBuilder.class);
+
+      mockedBuilder.when(() -> 
JWKSourceBuilder.create(any(URL.class))).thenReturn(mockBuilder);
+      when(mockBuilder.build()).thenReturn(mockJwkSource);
+      when(mockJwkSource.get(any(), any())).thenReturn(Arrays.asList(rsaKey));
+
+      // Initialize validator with pattern that requires @ (won't match)
+      Map<String, String> config = new HashMap<>();
+      config.put(
+          "gravitino.authenticator.oauth.jwksUri", 
"https://test-jwks.com/.well-known/jwks.json";);
+      config.put("gravitino.authenticator.oauth.authority", 
"https://test-issuer.com";);
+      config.put("gravitino.authenticator.oauth.principalMapper", "regex");
+      
config.put("gravitino.authenticator.oauth.principalMapper.regex.pattern", 
"([^@]+)@.*");
+      config.put("gravitino.authenticator.oauth.allowSkewSecs", "120");
+
+      validator.initialize(createConfig(config));
+      Principal result = validator.validateToken(tokenString, "test-service");
+
+      assertNotNull(result);
+      assertEquals("plainuser", result.getName());
+    }
+  }
 }
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestStaticSignKeyValidator.java
 
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestStaticSignKeyValidator.java
index 4ef5cda368..4512c93681 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestStaticSignKeyValidator.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestStaticSignKeyValidator.java
@@ -253,4 +253,236 @@ public class TestStaticSignKeyValidator {
     assertNotNull(principal);
     assertEquals("test-user", principal.getName());
   }
+
+  @Test
+  public void testPrincipalMapperWithDefaultPattern() {
+    Map<String, String> config = createBaseConfig();
+    config.put("gravitino.authenticator.oauth.principalMapper", "regex");
+    config.put("gravitino.authenticator.oauth.principalMapper.regex.pattern", 
"(.*)");
+    validator.initialize(createConfig(config));
+
+    String token =
+        Jwts.builder()
+            .setSubject("[email protected]")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    Principal principal = validator.validateToken(token, serviceAudience);
+    assertNotNull(principal);
+    assertEquals("[email protected]", principal.getName());
+  }
+
+  @Test
+  public void testPrincipalMapperExtractEmailLocalPart() {
+    Map<String, String> config = createBaseConfig();
+    config.put("gravitino.authenticator.oauth.principalMapper", "regex");
+    config.put("gravitino.authenticator.oauth.principalMapper.regex.pattern", 
"([^@]+)@.*");
+    validator.initialize(createConfig(config));
+
+    String token =
+        Jwts.builder()
+            .setSubject("[email protected]")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    Principal principal = validator.validateToken(token, serviceAudience);
+    assertNotNull(principal);
+    assertEquals("john.doe", principal.getName());
+  }
+
+  @Test
+  public void testPrincipalMapperNoMatch() {
+    Map<String, String> config = createBaseConfig();
+    config.put("gravitino.authenticator.oauth.principalMapper", "regex");
+    config.put("gravitino.authenticator.oauth.principalMapper.regex.pattern", 
"([^@]+)@.*");
+    validator.initialize(createConfig(config));
+
+    String token =
+        Jwts.builder()
+            .setSubject("plainuser")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    Principal principal = validator.validateToken(token, serviceAudience);
+    assertNotNull(principal);
+    assertEquals("plainuser", principal.getName());
+  }
+
+  @Test
+  public void testPrincipalMapperKerberosPrincipal() {
+    Map<String, String> config = createBaseConfig();
+    config.put("gravitino.authenticator.oauth.principalMapper", "regex");
+    config.put("gravitino.authenticator.oauth.principalMapper.regex.pattern", 
"([^/@]+).*");
+    validator.initialize(createConfig(config));
+
+    String token =
+        Jwts.builder()
+            .setSubject("admin/[email protected]")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    Principal principal = validator.validateToken(token, serviceAudience);
+    assertNotNull(principal);
+    assertEquals("admin", principal.getName());
+  }
+
+  @Test
+  public void testCustomPrincipalFieldsDefault() {
+    // Test default behavior: uses "sub" claim
+    Map<String, String> config = createBaseConfig();
+    // Don't set principalFields, should default to ["sub"]
+    validator.initialize(createConfig(config));
+
+    String token =
+        Jwts.builder()
+            .setSubject("default-subject")
+            .claim("client_id", "custom-client-123")
+            .claim("email", "[email protected]")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    Principal principal = validator.validateToken(token, serviceAudience);
+    assertNotNull(principal);
+    assertEquals("default-subject", principal.getName());
+  }
+
+  @Test
+  public void testCustomPrincipalFieldsSingleCustomField() {
+    // Test using a single custom field
+    Map<String, String> config = createBaseConfig();
+    config.put("gravitino.authenticator.oauth.principalFields", "email");
+    validator.initialize(createConfig(config));
+
+    String token =
+        Jwts.builder()
+            .setSubject("default-subject")
+            .claim("email", "[email protected]")
+            .claim("client_id", "custom-client-123")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    Principal principal = validator.validateToken(token, serviceAudience);
+    assertNotNull(principal);
+    assertEquals("[email protected]", principal.getName());
+  }
+
+  @Test
+  public void testCustomPrincipalFieldsFallback() {
+    // Test fallback: tries preferred_username first, then email, then sub
+    Map<String, String> config = createBaseConfig();
+    config.put("gravitino.authenticator.oauth.principalFields", 
"preferred_username,email,sub");
+    validator.initialize(createConfig(config));
+
+    // Token without preferred_username, should fall back to email
+    String token1 =
+        Jwts.builder()
+            .setSubject("default-subject")
+            .claim("email", "[email protected]")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    Principal principal1 = validator.validateToken(token1, serviceAudience);
+    assertNotNull(principal1);
+    assertEquals("[email protected]", principal1.getName());
+
+    // Token with preferred_username, should use it
+    String token2 =
+        Jwts.builder()
+            .setSubject("default-subject")
+            .claim("preferred_username", "john.doe")
+            .claim("email", "[email protected]")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    Principal principal2 = validator.validateToken(token2, serviceAudience);
+    assertNotNull(principal2);
+    assertEquals("john.doe", principal2.getName());
+
+    // Token with only sub, should fall back to sub
+    String token3 =
+        Jwts.builder()
+            .setSubject("only-subject")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    Principal principal3 = validator.validateToken(token3, serviceAudience);
+    assertNotNull(principal3);
+    assertEquals("only-subject", principal3.getName());
+  }
+
+  @Test
+  public void testCustomPrincipalFieldsNoMatch() {
+    // Test when no configured field is present in token
+    Map<String, String> config = createBaseConfig();
+    config.put("gravitino.authenticator.oauth.principalFields", 
"preferred_username,email");
+    validator.initialize(createConfig(config));
+
+    // Token without any of the configured fields
+    String token =
+        Jwts.builder()
+            .setSubject("default-subject")
+            .claim("client_id", "custom-client-123")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    assertThrows(
+        UnauthorizedException.class, () -> validator.validateToken(token, 
serviceAudience));
+  }
+
+  @Test
+  public void testPrincipalFieldsWithMapper() {
+    // Test that principal fields and mapper work together
+    Map<String, String> config = createBaseConfig();
+    config.put("gravitino.authenticator.oauth.principalFields", "email");
+    config.put("gravitino.authenticator.oauth.principalMapper", "regex");
+    config.put(
+        "gravitino.authenticator.oauth.principalMapper.regex.pattern",
+        "([^@]+)@.*"); // Extract user ID part
+
+    validator.initialize(createConfig(config));
+
+    String token =
+        Jwts.builder()
+            .setSubject("default-subject")
+            .claim("email", "[email protected]")
+            .setAudience(serviceAudience)
+            .setIssuedAt(Date.from(Instant.now()))
+            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
+            .signWith(hmacKey, SignatureAlgorithm.HS256)
+            .compact();
+
+    Principal principal = validator.validateToken(token, serviceAudience);
+    assertNotNull(principal);
+    assertEquals("john.doe", principal.getName()); // Mapper extracted local 
part
+  }
 }

Reply via email to