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 ebdc50164b [#10410] improve(authn) : OAuth2 authentication supports
obtaining user group information (#10503)
ebdc50164b is described below
commit ebdc50164b21884d4b6de6fcd4528de4b25c4d6e
Author: MaSai <[email protected]>
AuthorDate: Wed Mar 25 21:56:33 2026 +0800
[#10410] improve(authn) : OAuth2 authentication supports obtaining user
group information (#10503)
### What changes were proposed in this pull request?
This Pull Request supports OAuth2 authentication to obtain user group
information within the token.
### Why are the changes needed?
The user group information contained in the token also needs to be
authenticated. This Pull Request adds the ability to get user groups
within the token to prevent permission loss.
Fix: #10410
### Does this PR introduce any user-facing change?
When user set gravitino.authenticators = oauth and make token contains
group infos,gravitino will retrieve group infos from token
### How was this patch tested?
ITS
---
.../apache/gravitino/config/ConfigConstants.java | 3 +
.../main/java/org/apache/gravitino/UserGroup.java | 90 +++++++++++++++
.../java/org/apache/gravitino/UserPrincipal.java | 42 +++++--
.../org/apache/gravitino/auth/GroupMapper.java | 40 +++++++
.../apache/gravitino/auth/GroupMapperFactory.java | 63 +++++++++++
.../apache/gravitino/auth/RegexGroupMapper.java | 102 +++++++++++++++++
.../gravitino/auth/TestGroupMapperFactory.java | 125 +++++++++++++++++++++
docs/security/how-to-authenticate.md | 86 +++++++++++++-
.../server/authentication/JwksTokenValidator.java | 44 +++++++-
.../server/authentication/OAuthConfig.java | 31 +++++
.../authentication/StaticSignKeyValidator.java | 45 +++++++-
.../authentication/TestJwksTokenValidator.java | 120 ++++++++++++++++++++
.../authentication/TestStaticSignKeyValidator.java | 101 +++++++++++++++++
13 files changed, 879 insertions(+), 13 deletions(-)
diff --git
a/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
b/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
index fe1693267e..c6a4872cfc 100644
--- a/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
+++ b/common/src/main/java/org/apache/gravitino/config/ConfigConstants.java
@@ -89,6 +89,9 @@ public final class ConfigConstants {
/** The version number for the 1.2.0 release. */
public static final String VERSION_1_2_0 = "1.2.0";
+ /** The version number for the 1.3.0 release. */
+ public static final String VERSION_1_3_0 = "1.3.0";
+
/** The current version of backend storage initialization script. */
public static final String CURRENT_SCRIPT_VERSION = VERSION_1_2_0;
}
diff --git a/core/src/main/java/org/apache/gravitino/UserGroup.java
b/core/src/main/java/org/apache/gravitino/UserGroup.java
new file mode 100644
index 0000000000..30e6109cd8
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/UserGroup.java
@@ -0,0 +1,90 @@
+/*
+ * 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;
+
+import com.google.common.base.Preconditions;
+import java.util.Objects;
+import java.util.Optional;
+
+/** A class representing a user group with external UID and name. */
+public class UserGroup {
+
+ private final Optional<String> groupExternalUID;
+ private final String groupname;
+
+ /**
+ * Constructs a UserGroup instance.
+ *
+ * @param groupExternalUID The external UID of the group.
+ * @param groupname The name of the group.
+ */
+ public UserGroup(Optional<String> groupExternalUID, String groupname) {
+ Preconditions.checkArgument(groupname != null, "groupname cannot be null");
+ this.groupExternalUID = groupExternalUID;
+ this.groupname = groupname;
+ }
+
+ /**
+ * Returns the external UID of the group.
+ *
+ * @return The group external UID.
+ */
+ public Optional<String> getGroupExternalUID() {
+ return groupExternalUID;
+ }
+
+ /**
+ * Returns the name of the group.
+ *
+ * @return The group name.
+ */
+ public String getGroupname() {
+ return groupname;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof UserGroup)) {
+ return false;
+ }
+ UserGroup userGroup = (UserGroup) o;
+ return Objects.equals(groupExternalUID, userGroup.groupExternalUID)
+ && Objects.equals(groupname, userGroup.groupname);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(groupExternalUID, groupname);
+ }
+
+ @Override
+ public String toString() {
+ return "UserGroup{"
+ + "groupExternalUID="
+ + groupExternalUID
+ + ", groupname='"
+ + groupname
+ + '\''
+ + '}';
+ }
+}
diff --git a/core/src/main/java/org/apache/gravitino/UserPrincipal.java
b/core/src/main/java/org/apache/gravitino/UserPrincipal.java
index a1d00f7ccc..296dedaa56 100644
--- a/core/src/main/java/org/apache/gravitino/UserPrincipal.java
+++ b/core/src/main/java/org/apache/gravitino/UserPrincipal.java
@@ -21,11 +21,16 @@ package org.apache.gravitino;
import com.google.common.base.Preconditions;
import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
-/** A simple implementation of Principal that holds a username. */
+/** A simple implementation of Principal that holds a username and group
membership. */
public class UserPrincipal implements Principal {
private final String username;
+ private final List<UserGroup> groups;
/**
* Constructs a UserPrincipal with the given username.
@@ -33,8 +38,22 @@ public class UserPrincipal implements Principal {
* @param username the username of the principal
*/
public UserPrincipal(final String username) {
+ this(username, Collections.emptyList());
+ }
+
+ /**
+ * Constructs a UserPrincipal with the given username and groups.
+ *
+ * @param username the username of the principal
+ * @param groups the groups of the principal
+ */
+ public UserPrincipal(final String username, final List<UserGroup> groups) {
Preconditions.checkArgument(username != null, "UserPrincipal must have the
username");
this.username = username;
+ this.groups =
+ groups != null
+ ? Collections.unmodifiableList(new ArrayList<>(groups))
+ : Collections.emptyList();
}
/**
@@ -47,9 +66,18 @@ public class UserPrincipal implements Principal {
return username;
}
+ /**
+ * Returns the groups of this principal.
+ *
+ * @return the groups
+ */
+ public List<UserGroup> getGroups() {
+ return groups;
+ }
+
@Override
public int hashCode() {
- return username.hashCode();
+ return Objects.hash(username, groups);
}
@Override
@@ -57,15 +85,15 @@ public class UserPrincipal implements Principal {
if (this == o) {
return true;
}
- if (o instanceof UserPrincipal) {
- UserPrincipal that = (UserPrincipal) o;
- return this.username.equals(that.username);
+ if (!(o instanceof UserPrincipal)) {
+ return false;
}
- return false;
+ UserPrincipal that = (UserPrincipal) o;
+ return Objects.equals(username, that.username) && Objects.equals(groups,
that.groups);
}
@Override
public String toString() {
- return "[principal: " + this.username + "]";
+ return "[principal: " + this.username + ", groups: " + this.groups + "]";
}
}
diff --git a/core/src/main/java/org/apache/gravitino/auth/GroupMapper.java
b/core/src/main/java/org/apache/gravitino/auth/GroupMapper.java
new file mode 100644
index 0000000000..65acf7b7d2
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/auth/GroupMapper.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.util.List;
+import org.apache.gravitino.UserGroup;
+
+/**
+ * Interface for mapping authenticated groups.
+ *
+ * <p>Implementations should be thread-safe as they may be shared across
multiple authentication
+ * requests.
+ */
+public interface GroupMapper {
+
+ /**
+ * Maps a list of group strings to a new list of group strings.
+ *
+ * @param groups the list of group strings to map
+ * @return a list of mapped group strings
+ */
+ List<UserGroup> map(List<Object> groups);
+}
diff --git
a/core/src/main/java/org/apache/gravitino/auth/GroupMapperFactory.java
b/core/src/main/java/org/apache/gravitino/auth/GroupMapperFactory.java
new file mode 100644
index 0000000000..d66cd46f91
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/auth/GroupMapperFactory.java
@@ -0,0 +1,63 @@
+/*
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Factory class for creating {@link GroupMapper} instances. */
+public class GroupMapperFactory {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(GroupMapperFactory.class);
+
+ private GroupMapperFactory() {}
+
+ /**
+ * Creates a GroupMapper instance based on the configuration.
+ *
+ * @param mapperType the type of the mapper (e.g., "regex" or fully
qualified class name)
+ * @param regexPattern the regex pattern to use (only for "regex" mapper
type)
+ * @return a configured GroupMapper instance
+ * @throws IllegalArgumentException if the mapper type is invalid or
initialization fails
+ */
+ public static GroupMapper create(String mapperType, String regexPattern) {
+ if ("regex".equalsIgnoreCase(mapperType)) {
+ if (regexPattern == null) {
+ throw new IllegalArgumentException("Regex pattern cannot be null for
regex mapper");
+ }
+ return new RegexGroupMapper(regexPattern);
+ }
+
+ try {
+ Class<?> clazz = Class.forName(mapperType);
+ if (!GroupMapper.class.isAssignableFrom(clazz)) {
+ throw new IllegalArgumentException(
+ "Class " + mapperType + " does not implement GroupMapper");
+ }
+ return (GroupMapper) clazz.getDeclaredConstructor().newInstance();
+ } catch (ClassNotFoundException e) {
+ LOG.error("Failed to load GroupMapper class: {}", mapperType, e);
+ throw new IllegalArgumentException("Failed to load GroupMapper class: "
+ mapperType, e);
+ } catch (Exception e) {
+ LOG.error("Failed to create GroupMapper: {}", mapperType, e);
+ throw new IllegalArgumentException("Failed to create GroupMapper: " +
mapperType, e);
+ }
+ }
+}
diff --git a/core/src/main/java/org/apache/gravitino/auth/RegexGroupMapper.java
b/core/src/main/java/org/apache/gravitino/auth/RegexGroupMapper.java
new file mode 100644
index 0000000000..9fc0eefc5d
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/auth/RegexGroupMapper.java
@@ -0,0 +1,102 @@
+/*
+ * 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 com.google.common.base.Preconditions;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.gravitino.UserGroup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Regex-based group mapper that extracts group names using regex patterns
with capturing groups.
+ *
+ * <p>This implementation is thread-safe as Pattern.matcher() creates
thread-local Matcher
+ * instances.
+ */
+public class RegexGroupMapper implements GroupMapper {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(RegexGroupMapper.class);
+
+ private final Pattern pattern;
+
+ /**
+ * Creates a new regex group mapper.
+ *
+ * @param patternStr the regex pattern with a capturing group (required)
+ * @throws IllegalArgumentException if the pattern string has invalid regex
syntax
+ */
+ public RegexGroupMapper(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 RegexGroupMapper with pattern: {}", patternStr);
+ }
+
+ /**
+ * Maps a list of group strings to a new list of group strings using the
configured regex pattern.
+ *
+ * @param groups the list of group strings to map
+ * @return a list of mapped group strings
+ */
+ @Override
+ public List<UserGroup> map(List<Object> groups) {
+ if (groups == null || groups.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ groups.forEach(
+ g ->
+ Preconditions.checkArgument(
+ g == null || g instanceof String, "Group must be a string"));
+ List<UserGroup> mappedGroups = new ArrayList<>();
+ for (Object groupObj : groups) {
+ if (groupObj == null) {
+ continue;
+ }
+ String group = (String) groupObj;
+ try {
+ Matcher matcher = pattern.matcher(group);
+ if (matcher.find() && matcher.groupCount() >= 1) {
+ String extracted = matcher.group(1);
+ if (extracted != null && !extracted.isEmpty()) {
+ mappedGroups.add(new UserGroup(Optional.empty(), extracted));
+ } else {
+ mappedGroups.add(new UserGroup(Optional.empty(), group));
+ }
+ } else {
+ mappedGroups.add(new UserGroup(Optional.empty(), group));
+ }
+ } catch (Exception e) {
+ String message =
+ String.format(
+ "Error applying regex pattern '%s' to group '%s'",
pattern.pattern(), group);
+ LOG.error("{}: {}", message, e.getMessage());
+ throw new IllegalArgumentException(message, e);
+ }
+ }
+ return mappedGroups;
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/auth/TestGroupMapperFactory.java
b/core/src/test/java/org/apache/gravitino/auth/TestGroupMapperFactory.java
new file mode 100644
index 0000000000..35dc34c8e5
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/auth/TestGroupMapperFactory.java
@@ -0,0 +1,125 @@
+/*
+ * 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.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.gravitino.UserGroup;
+import org.junit.jupiter.api.Test;
+
+public class TestGroupMapperFactory {
+
+ @Test
+ public void testCreateRegexMapper() {
+ GroupMapper mapper = GroupMapperFactory.create("regex", "group-(.*)");
+
+ assertNotNull(mapper);
+ assertTrue(mapper instanceof RegexGroupMapper);
+
+ List<Object> groups = Arrays.asList("group-admin", "group-user");
+ List<UserGroup> mappedGroups = mapper.map(groups);
+
+ assertEquals(2, mappedGroups.size());
+ List<String> groupNames =
+
mappedGroups.stream().map(UserGroup::getGroupname).collect(Collectors.toList());
+ assertTrue(groupNames.contains("admin"));
+ assertTrue(groupNames.contains("user"));
+ }
+
+ @Test
+ public void testCreateRegexMapperWithDefaultPattern() {
+ GroupMapper mapper = GroupMapperFactory.create("regex", "^(.*)$");
+
+ assertNotNull(mapper);
+ assertTrue(mapper instanceof RegexGroupMapper);
+
+ List<Object> groups = Arrays.asList("admin", "user");
+ List<UserGroup> mappedGroups = mapper.map(groups);
+
+ assertEquals(2, mappedGroups.size());
+ List<String> groupNames =
+
mappedGroups.stream().map(UserGroup::getGroupname).collect(Collectors.toList());
+ assertTrue(groupNames.contains("admin"));
+ assertTrue(groupNames.contains("user"));
+ }
+
+ @Test
+ public void testCreateRegexMapperWithSlash() {
+ GroupMapper mapper = GroupMapperFactory.create("regex", "/(.*)");
+
+ assertNotNull(mapper);
+ assertTrue(mapper instanceof RegexGroupMapper);
+
+ List<Object> groups = Arrays.asList("/admin", "/user");
+ List<UserGroup> mappedGroups = mapper.map(groups);
+
+ assertEquals(2, mappedGroups.size());
+ List<String> groupNames =
+
mappedGroups.stream().map(UserGroup::getGroupname).collect(Collectors.toList());
+ assertTrue(groupNames.contains("admin"));
+ assertTrue(groupNames.contains("user"));
+ }
+
+ @Test
+ public void testCreateCustomMapperWithInvalidClass() {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ GroupMapperFactory.create("unknown.InvalidClass", null);
+ });
+ assertTrue(exception.getMessage().contains("Failed to load GroupMapper
class"));
+ }
+
+ public static class TestCustomGroupMapper implements GroupMapper {
+ @Override
+ public List<UserGroup> map(List<Object> groups) {
+ if (groups == null) {
+ return Collections.emptyList();
+ }
+ return groups.stream()
+ .map(g -> new UserGroup(Optional.empty(), "custom:" + g.toString()))
+ .collect(Collectors.toList());
+ }
+ }
+
+ @Test
+ public void testCreateCustomMapperWithInlineClass() {
+ String className = TestCustomGroupMapper.class.getName();
+ GroupMapper mapper = GroupMapperFactory.create(className, null);
+
+ assertNotNull(mapper);
+ assertTrue(mapper instanceof TestCustomGroupMapper);
+
+ List<Object> groups = Collections.singletonList("foo");
+ List<UserGroup> mappedGroups = mapper.map(groups);
+
+ assertEquals(1, mappedGroups.size());
+ assertEquals("custom:foo", mappedGroups.get(0).getGroupname());
+ }
+}
diff --git a/docs/security/how-to-authenticate.md
b/docs/security/how-to-authenticate.md
index dc7268f78d..54dab3bac9 100644
--- a/docs/security/how-to-authenticate.md
+++ b/docs/security/how-to-authenticate.md
@@ -117,6 +117,61 @@ GravitinoClient client = GravitinoClient.builder(uri)
Gravitino supports principal mapping to transform authenticated principals
(from OAuth or Kerberos) into user identities for authorization. By default,
Gravitino uses regex-based mapping.
+### Group mapping
+
+Gravitino supports group mapping to transform authenticated groups (from
OAuth) into Gravitino groups for authorization. By default, Gravitino uses
regex-based mapping.
+
+#### OAuth group mapping
+
+For OAuth authentication, groups are extracted from JWT claims (configured via
`gravitino.authenticator.oauth.groupsFields`). You can customize how these
groups are mapped:
+
+```text
+# Use default regex mapper that extracts everything (passes through unchanged)
+gravitino.authenticator.oauth.groupMapper = regex
+gravitino.authenticator.oauth.groupMapper.regex.pattern = ^(.*)$
+
+# Extract group from a complex string (e.g., /group -> group)
+gravitino.authenticator.oauth.groupMapper = regex
+gravitino.authenticator.oauth.groupMapper.regex.pattern = ^/(.*)
+
+
+# Use custom group mapper implementation
+gravitino.authenticator.oauth.groupMapper = com.example.MyCustomGroupMapper
+```
+
+#### Custom group mapper
+
+For advanced use cases, implement the `GroupMapper` interface:
+
+```java
+package com.example;
+
+import org.apache.gravitino.UserGroup;
+import org.apache.gravitino.auth.GroupMapper;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class MyCustomGroupMapper implements GroupMapper {
+ @Override
+ public List<UserGroup> map(List<Object> groups) {
+ if (groups == null) {
+ return Collections.emptyList();
+ }
+ return groups.stream()
+ .map(g -> new UserGroup(Optional.empty(), "mapped_" + g.toString()))
+ .collect(Collectors.toList());
+ }
+}
+```
+
+Configure Gravitino to use your custom mapper:
+
+```text
+gravitino.authenticator.oauth.groupMapper = com.example.MyCustomGroupMapper
+```
+
#### 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:
@@ -131,7 +186,31 @@ gravitino.authenticator.oauth.principalMapper = regex
gravitino.authenticator.oauth.principalMapper.regex.pattern = ([^@]+)@.*
# Use custom mapper implementation
-gravitino.authenticator.oauth.principalMapper = com.example.MyCustomMapper
+gravitino.authenticator.oauth.principalMapper =
com.example.MyCustomPrincipalMapper
+```
+
+#### Custom principal mapper
+
+For advanced use cases, implement the `PrincipalMapper` interface:
+
+```java
+package com.example;
+
+import org.apache.gravitino.auth.PrincipalMapper;
+import java.security.Principal;
+
+public class MyCustomPrincipalMapper implements PrincipalMapper {
+ @Override
+ public Principal map(String principal) {
+ return () -> "mapped_" + principal;
+ }
+}
+```
+
+Configure Gravitino to use your custom mapper:
+
+```text
+gravitino.authenticator.oauth.principalMapper =
com.example.MyCustomPrincipalMapper
```
#### Kerberos principal mapping
@@ -148,7 +227,7 @@ gravitino.authenticator.kerberos.principalMapper = regex
gravitino.authenticator.kerberos.principalMapper.regex.pattern = ([^/@]+).*
```
-#### Custom principal mapper
+#### Custom Kerberos principal mapper
For advanced use cases, implement the `PrincipalMapper` interface:
@@ -208,9 +287,12 @@ Gravitino server and Gravitino Iceberg REST server share
the same configuration
| `gravitino.authenticator.oauth.scope` | OAuth scopes for Web
UI authentication (space-separated).
| (none) | Yes
if provider is `oidc`
| 1. [...]
| `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.groupsFields` | JWT claim field(s) to
use as group membership. Comma-separated list for fallback in order (e.g.,
'groups,roles').
| `groups` |
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.oauth.groupMapper` | Group mapper type for OAuth.
Use 'regex' for regex-based mapping, or provide a fully qualified class name
implementing `org.apache.gravitino.auth.GroupMapper`.
|
`regex` | No
| 1.3.0 [...]
+| `gravitino.authenticator.oauth.groupMapper.regex.pattern` | Regex pattern
for OAuth group mapping. First capture group becomes the mapped group. Only
used when groupMapper is 'regex'.
| `^(.*)$` |
No
| 1. [...]
| `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 [...]
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 e358a8998d..7365d8b14d 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,6 +36,10 @@ import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Config;
+import org.apache.gravitino.UserGroup;
+import org.apache.gravitino.UserPrincipal;
+import org.apache.gravitino.auth.GroupMapper;
+import org.apache.gravitino.auth.GroupMapperFactory;
import org.apache.gravitino.auth.PrincipalMapper;
import org.apache.gravitino.auth.PrincipalMapperFactory;
import org.apache.gravitino.exceptions.UnauthorizedException;
@@ -55,14 +59,17 @@ public class JwksTokenValidator implements
OAuthTokenValidator {
private String jwksUri;
private String expectedIssuer;
private List<String> principalFields;
+ private List<String> groupsFields;
private long allowSkewSeconds;
private PrincipalMapper principalMapper;
+ private GroupMapper groupMapper;
@Override
public void initialize(Config config) {
this.jwksUri = config.get(OAuthConfig.JWKS_URI);
this.expectedIssuer = config.get(OAuthConfig.AUTHORITY);
this.principalFields = config.get(OAuthConfig.PRINCIPAL_FIELDS);
+ this.groupsFields = config.get(OAuthConfig.GROUPS_FIELDS);
this.allowSkewSeconds = config.get(OAuthConfig.ALLOW_SKEW_SECONDS);
// Create principal mapper based on configuration
@@ -70,6 +77,11 @@ public class JwksTokenValidator implements
OAuthTokenValidator {
String regexPattern =
config.get(OAuthConfig.PRINCIPAL_MAPPER_REGEX_PATTERN);
this.principalMapper = PrincipalMapperFactory.create(mapperType,
regexPattern);
+ // Create group mapper based on configuration
+ String groupMapperType = config.get(OAuthConfig.GROUP_MAPPER);
+ String groupRegexPattern =
config.get(OAuthConfig.GROUP_MAPPER_REGEX_PATTERN);
+ this.groupMapper = GroupMapperFactory.create(groupMapperType,
groupRegexPattern);
+
LOG.info("Initializing JWKS token validator");
if (StringUtils.isBlank(jwksUri)) {
@@ -140,7 +152,13 @@ public class JwksTokenValidator implements
OAuthTokenValidator {
}
// Use principal mapper to extract username
- return principalMapper.map(principal);
+ Principal userPrincipal = principalMapper.map(principal);
+ List<Object> groups = extractGroups(validatedClaims);
+ if (groups != null && !groups.isEmpty()) {
+ List<UserGroup> mappedGroups = groupMapper.map(groups);
+ return new UserPrincipal(userPrincipal.getName(), mappedGroups);
+ }
+ return userPrincipal;
} catch (Exception e) {
LOG.error("JWKS JWT validation error: {}", e.getMessage());
@@ -164,9 +182,29 @@ public class JwksTokenValidator implements
OAuthTokenValidator {
if (principalFields != null && !principalFields.isEmpty()) {
for (String field : principalFields) {
if (StringUtils.isNotBlank(field)) {
- String principal = (String) validatedClaims.getClaim(field);
+ Object principal = validatedClaims.getClaim(field);
if (principal != null) {
- return principal;
+ return principal.toString();
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Extracts the groups from the validated JWT claims using configured
field(s). */
+ private List<Object> extractGroups(JWTClaimsSet validatedClaims) {
+ if (groupsFields != null && !groupsFields.isEmpty()) {
+ for (String field : groupsFields) {
+ if (StringUtils.isNotBlank(field)) {
+ try {
+ Object groupsObj = validatedClaims.getClaim(field);
+ if (groupsObj instanceof List) {
+ return (List<Object>) groupsObj;
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to parse groups from claim field: {}", field, e);
}
}
}
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 3d3bfdbfa8..2a9bc5eddb 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
@@ -138,6 +138,15 @@ public interface OAuthConfig {
.toSequence()
.createWithDefault(java.util.Arrays.asList("sub"));
+ ConfigEntry<List<String>> GROUPS_FIELDS =
+ new ConfigBuilder(OAUTH_CONFIG_PREFIX + "groupsFields")
+ .doc(
+ "JWT claim field(s) to use as groups. Comma-separated list for
fallback in order (e.g., 'groups,roles').")
+ .version(ConfigConstants.VERSION_1_3_0)
+ .stringConf()
+ .toSequence()
+ .createWithDefault(java.util.Arrays.asList("groups"));
+
ConfigEntry<String> TOKEN_VALIDATOR_CLASS =
new ConfigBuilder(OAUTH_CONFIG_PREFIX + "tokenValidatorClass")
.doc("Fully qualified class name of the OAuth token validator
implementation")
@@ -166,4 +175,26 @@ public interface OAuthConfig {
.version(ConfigConstants.VERSION_1_2_0)
.stringConf()
.createWithDefault("^(.*)$");
+
+ ConfigEntry<String> GROUP_MAPPER =
+ new ConfigBuilder(OAUTH_CONFIG_PREFIX + "groupMapper")
+ .doc(
+ "Type of group mapper to use for OAuth/JWT groups. "
+ + "Built-in value: 'regex' (uses regex pattern to extract
group). "
+ + "Default pattern '^(.*)$' keeps the group unchanged. "
+ + "Can also be a fully qualified class name implementing
GroupMapper for custom logic.")
+ .version(ConfigConstants.VERSION_1_3_0)
+ .stringConf()
+ .createWithDefault("regex");
+
+ ConfigEntry<String> GROUP_MAPPER_REGEX_PATTERN =
+ new ConfigBuilder(OAUTH_CONFIG_PREFIX + "groupMapper.regex.pattern")
+ .doc(
+ "Regex pattern to extract the group from the OAuth group field. "
+ + "Only used when groupMapper is 'regex'. "
+ + "The pattern should contain at least one capturing group. "
+ + "Default pattern '^(.*)$' matches the entire group.")
+ .version(ConfigConstants.VERSION_1_3_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 a6e94693f6..a08e208bba 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,10 +38,16 @@ import java.util.Base64;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Config;
+import org.apache.gravitino.UserGroup;
+import org.apache.gravitino.UserPrincipal;
+import org.apache.gravitino.auth.GroupMapper;
+import org.apache.gravitino.auth.GroupMapperFactory;
import org.apache.gravitino.auth.PrincipalMapper;
import org.apache.gravitino.auth.PrincipalMapperFactory;
import org.apache.gravitino.auth.SignatureAlgorithmFamilyType;
import org.apache.gravitino.exceptions.UnauthorizedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Static OAuth token validator that uses a pre-configured signing key for JWT
validation.
@@ -51,10 +57,14 @@ import
org.apache.gravitino.exceptions.UnauthorizedException;
* signAlgorithmType} for validation.
*/
public class StaticSignKeyValidator implements OAuthTokenValidator {
+ private static final Logger LOG =
LoggerFactory.getLogger(StaticSignKeyValidator.class);
+
private long allowSkewSeconds;
private Key defaultSigningKey;
private PrincipalMapper principalMapper;
private List<String> principalFields;
+ private GroupMapper groupMapper;
+ private List<String> groupsFields;
@Override
public void initialize(Config config) {
@@ -69,10 +79,17 @@ public class StaticSignKeyValidator implements
OAuthTokenValidator {
String algType = config.get(OAuthConfig.SIGNATURE_ALGORITHM_TYPE);
this.defaultSigningKey =
decodeSignKey(Base64.getDecoder().decode(configuredSignKey), algType);
this.principalFields = config.get(OAuthConfig.PRINCIPAL_FIELDS);
+ this.groupsFields = config.get(OAuthConfig.GROUPS_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);
+
+ // Create group mapper based on configuration
+ String groupMapperType = config.get(OAuthConfig.GROUP_MAPPER);
+ String groupRegexPattern =
config.get(OAuthConfig.GROUP_MAPPER_REGEX_PATTERN);
+ this.groupMapper = GroupMapperFactory.create(groupMapperType,
groupRegexPattern);
}
@Override
@@ -110,7 +127,13 @@ public class StaticSignKeyValidator implements
OAuthTokenValidator {
String principal = extractPrincipal(jwt.getBody());
// Use principal mapper to extract username
- return principalMapper.map(principal);
+ Principal userPrincipal = principalMapper.map(principal);
+ List<Object> groups = extractGroups(jwt.getBody());
+ if (groups != null && !groups.isEmpty()) {
+ List<UserGroup> mappedGroups = groupMapper.map(groups);
+ return new UserPrincipal(userPrincipal.getName(), mappedGroups);
+ }
+ return userPrincipal;
} catch (ExpiredJwtException
| UnsupportedJwtException
| MalformedJwtException
@@ -138,6 +161,26 @@ public class StaticSignKeyValidator implements
OAuthTokenValidator {
"No valid principal found in token. Checked fields: %s",
principalFields);
}
+ /** Extracts the groups from the validated JWT claims using configured
field(s). */
+ private List<Object> extractGroups(Claims claims) {
+ if (groupsFields != null && !groupsFields.isEmpty()) {
+ for (String field : groupsFields) {
+ if (StringUtils.isNotBlank(field)) {
+ try {
+ Object groupsObj = claims.get(field);
+ if (groupsObj instanceof List) {
+ return (List<Object>) groupsObj;
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to parse groups from claim field: {}", field, e);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
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 3068b5aee5..dc4b63a202 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
@@ -548,4 +548,124 @@ public class TestJwksTokenValidator {
assertEquals("plainuser", result.getName());
}
}
+
+ @Test
+ public void testGroupsClaimPresent() 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 groups claim
+ JWTClaimsSet claimsSet =
+ new JWTClaimsSet.Builder()
+ .subject("test-subject")
+ .claim("groups", Arrays.asList("group1", "group2"))
+ .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);
+
+ // Configure the mock to return our test key
+ when(mockJwkSource.get(any(), any())).thenReturn(Arrays.asList(rsaKey));
+
+ // Initialize validator with groups field configured
+ 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.groupsFields", "groups");
+ config.put("gravitino.authenticator.oauth.allowSkewSecs", "120");
+
+ validator.initialize(createConfig(config));
+ Principal result = validator.validateToken(tokenString, "test-service");
+
+ assertNotNull(result);
+ assertTrue(result instanceof UserPrincipal);
+ UserPrincipal userPrincipal = (UserPrincipal) result;
+ assertEquals("test-subject", userPrincipal.getName());
+ assertEquals(2, userPrincipal.getGroups().size());
+ assertTrue(
+ userPrincipal.getGroups().stream().anyMatch(g ->
g.getGroupname().equals("group1")));
+ assertTrue(
+ userPrincipal.getGroups().stream().anyMatch(g ->
g.getGroupname().equals("group2")));
+ }
+ }
+
+ @Test
+ public void testGroupsClaimMissing() 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 without groups claim
+ JWTClaimsSet claimsSet =
+ new JWTClaimsSet.Builder()
+ .subject("test-subject")
+ .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);
+
+ // Configure the mock to return our test key
+ when(mockJwkSource.get(any(), any())).thenReturn(Arrays.asList(rsaKey));
+
+ // Initialize validator with groups field configured
+ 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.groupsFields", "groups");
+ config.put("gravitino.authenticator.oauth.allowSkewSecs", "120");
+
+ validator.initialize(createConfig(config));
+ Principal result = validator.validateToken(tokenString, "test-service");
+
+ assertNotNull(result);
+ assertTrue(result instanceof UserPrincipal);
+ UserPrincipal userPrincipal = (UserPrincipal) result;
+ assertEquals("test-subject", userPrincipal.getName());
+ // Expect no groups if claim is missing
+ assertTrue(userPrincipal.getGroups().isEmpty());
+ }
+ }
}
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 4512c93681..0156dad6b4 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
@@ -20,8 +20,10 @@
package org.apache.gravitino.server.authentication;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
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 io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@@ -36,6 +38,7 @@ import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.gravitino.Config;
+import org.apache.gravitino.UserPrincipal;
import org.apache.gravitino.exceptions.UnauthorizedException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -485,4 +488,102 @@ public class TestStaticSignKeyValidator {
assertNotNull(principal);
assertEquals("john.doe", principal.getName()); // Mapper extracted local
part
}
+
+ @Test
+ public void testValidateTokenWithGroups() {
+ Map<String, String> config = createBaseConfig();
+ config.put("gravitino.authenticator.oauth.groupsFields", "groups");
+ validator.initialize(createConfig(config));
+
+ // Create token with groups
+ String token =
+ Jwts.builder()
+ .setSubject("test-user")
+ .claim("groups", Arrays.asList("group1", "group2"))
+ .setAudience(serviceAudience)
+ .setIssuedAt(Date.from(Instant.now()))
+ .setExpiration(Date.from(Instant.now().plusSeconds(315360000)))
+ .signWith(hmacKey, SignatureAlgorithm.HS256)
+ .compact();
+
+ Principal principal = validator.validateToken(token, serviceAudience);
+ assertNotNull(principal);
+ assertEquals("test-user", principal.getName());
+
+ // Check if principal is UserPrincipal and has groups
+ UserPrincipal userPrincipal = assertInstanceOf(UserPrincipal.class,
principal);
+ assertEquals(2, userPrincipal.getGroups().size());
+ assertTrue(userPrincipal.getGroups().stream().anyMatch(g ->
g.getGroupname().equals("group1")));
+ assertTrue(userPrincipal.getGroups().stream().anyMatch(g ->
g.getGroupname().equals("group2")));
+ }
+
+ @Test
+ public void testValidateRealKeycloakTokenWithGroups() {
+ Map<String, String> config = new HashMap<String, String>();
+ config.put("gravitino.authenticator.oauth.groupsFields", "groups");
+ config.put("gravitino.authenticator.oauth.groupMapper.regex.pattern",
"^/(.*)");
+ config.put("gravitino.authenticator.oauth.principalFields",
"preferred_username");
+ config.put("gravitino.authenticator.oauth.allowSkewSecs", "315360000");
+ config.put(
+ "gravitino.authenticator.oauth.defaultSignKey",
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0J4W"
+ +
"fjgnb/nyrkOuVefHoGu17pIpiUm/TaTeVKoZcYxphcL8HAg2jKTQYov4SvlWZ4wXHZ1FayPm5+e74W5vSGOgZzjyzMZKithGkYa"
+ +
"gbRuXbUeo4sw6f7t4BsAmgpAUgyBYE/1fdEghHtKHia1Er1grQVsvGVAAbYoFjh+OfcnRLYFAel+ADvX83RmsK16laxXpY+LeFk"
+ +
"jugOsCaD5mnGfgqQC96t9H4/QlsqJacg+9YImZWxUHtqN/itrV/VCdGVfywih6Dk9F2jxt4rL9++3xVdG/OrvFDtNW98xhKqgqF"
+ + "iXLsjKPT5WmqzbGCaQi/29Cdu9QOmd/IxmVeRy0+wIDAQAB");
+ config.put("gravitino.authenticator.oauth.serverUri",
"http://localhost:8080");
+ config.put(
+ "gravitino.authenticator.oauth.tokenPath",
+ "/realms/gravitinorealm/protocol/openid-connect/token");
+ config.put("gravitino.authenticator.oauth.signAlgorithmType", "RS256");
+
+ validator.initialize(createConfig(config));
+
+ // {
+ // "exp": 1774280470,
+ // "iat": 1774280170,
+ // "jti": "bdc771ca-b186-49a9-a3cd-9f16f5e6f96a",
+ // "iss": "http://localhost:8080/realms/gravitinorealm",
+ // "aud": "gravitino-client",
+ // "sub": "9db45582-a08c-4721-be12-6e5905d37317",
+ // "typ": "ID",
+ // "azp": "gravitino-client",
+ // "sid": "1710a207-72d6-42f8-b5c7-2d2b5fa8e52a",
+ // "at_hash": "WMloUfdrj1EZdrz3LhemPA",
+ // "acr": "1",
+ // "email_verified": false,
+ // "name": "a b",
+ // "groups": [
+ // "/groupa",
+ // "/groupb"
+ // ],
+ // "preferred_username": "userb",
+ // "given_name": "a",
+ // "family_name": "b",
+ // "email": "[email protected]"
+ // }
+ String token =
+
"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIySHlKQVZ6STJvQlVqUXdZUWZEbEh5SW1Ec3dWUGh1akJmLUZfbjZIUm"
+ +
"s4In0.eyJleHAiOjE3NzQyODA0NzAsImlhdCI6MTc3NDI4MDE3MCwianRpIjoiYmRjNzcxY2EtYjE4Ni00OWE5LWEzY2QtOWYx"
+ +
"NmY1ZTZmOTZhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9ncmF2aXRpbm9yZWFsbSIsImF1ZCI6ImdyYX"
+ +
"ZpdGluby1jbGllbnQiLCJzdWIiOiI5ZGI0NTU4Mi1hMDhjLTQ3MjEtYmUxMi02ZTU5MDVkMzczMTciLCJ0eXAiOiJJRCIsImF6"
+ +
"cCI6ImdyYXZpdGluby1jbGllbnQiLCJzaWQiOiIxNzEwYTIwNy03MmQ2LTQyZjgtYjVjNy0yZDJiNWZhOGU1MmEiLCJhdF9oYX"
+ +
"NoIjoiV01sb1VmZHJqMUVaZHJ6M0xoZW1QQSIsImFjciI6IjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJhIGIi"
+ +
"LCJncm91cHMiOlsiL2dyb3VwYSIsIi9ncm91cGIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlcmIiLCJnaXZlbl9uYW1lIj"
+ +
"oiYSIsImZhbWlseV9uYW1lIjoiYiIsImVtYWlsIjoic2FpQGRhdGFzdHJhdG8uY29tIn0.ZNYmcK1gFTONojvp5aWekp9eK89y"
+ +
"j7I1PszY4aK9y_dFF2Dp_Ec1K71LygCccem_mnopua2Ys92BwthgHrc4p1LoZ9nC98PIXrlhNw6BPazBELUgIol6HPzMQ18HvN"
+ +
"DO_K4pRzMK1khIcpTDgt5ikpw8-8i9KFAPyvO3ezKhIv5h4TQPdMV9RaAplP539hkuxSi4VhIlz6qYPuyoTW4QPmnqKbn7E0IE"
+ +
"EgVnpvQ7Y-xxoCstpFNhN_IikGDz7G7xQwBbYo4zvo4brixzRWOW-wWe9nLDTI9veYxs8Hs_ZG_JkhiCvecHNY811o_fgQ0Bz2"
+ + "mWizL5ukKRnxXfL0oPVg";
+
+ Principal principal = validator.validateToken(token, "gravitino-client");
+ assertNotNull(principal);
+ assertEquals("userb", principal.getName());
+
+ // Check if principal is UserPrincipal and has groups
+ UserPrincipal userPrincipal = assertInstanceOf(UserPrincipal.class,
principal);
+ assertEquals(2, userPrincipal.getGroups().size());
+ assertTrue(userPrincipal.getGroups().stream().anyMatch(g ->
g.getGroupname().equals("groupa")));
+ assertTrue(userPrincipal.getGroups().stream().anyMatch(g ->
g.getGroupname().equals("groupb")));
+ }
}