From 62025e51c45a0580730c2f2a92a2e962f8734742 Mon Sep 17 00:00:00 2001 From: Lukas Ziefle Date: Tue, 5 Mar 2024 13:18:01 +0100 Subject: [PATCH] Prioritize groups path over groups claim But fallback to the groups claim if the claim speficied in `smallrye.jwt.path.groups` is empty. If the groups claim is also empty, fallback to the default claim configured by `smallrye.jwt.claims.groups`. Signed-off-by: Lukas Ziefle --- .../jwt/auth/principal/PrincipalUtils.java | 63 ++++--- .../auth/principal/PrincipalUtilsTest.java | 161 ++++++++++++++++++ 2 files changed, 196 insertions(+), 28 deletions(-) create mode 100644 implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/PrincipalUtilsTest.java diff --git a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalUtils.java b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalUtils.java index 2ad7ce3f..131680b1 100644 --- a/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalUtils.java +++ b/implementation/jwt-auth/src/main/java/io/smallrye/jwt/auth/principal/PrincipalUtils.java @@ -46,15 +46,28 @@ public static void setClaims(JwtClaims claimsSet, String token, JWTAuthContextIn String sub = findSubject(authContextInfo, claimsSet); claimsSet.setClaim(Claims.sub.name(), sub); } - Object groupsClaim = claimsSet.getClaimValue(Claims.groups.name()); - if (groupsClaim == null || groupsClaim instanceof Map - || authContextInfo.getGroupsPath() != null) { // specified groups path takes precedence over existing groups and replaces them - List groups = findGroups(authContextInfo, claimsSet); - claimsSet.setClaim(Claims.groups.name(), groups); - } else if (groupsClaim instanceof String) { - claimsSet.setClaim(Claims.groups.name(), - splitStringClaimValue(groupsClaim.toString(), authContextInfo)); + + List roles = null; + if (authContextInfo.getGroupsPath() != null) { + roles = findGroups(authContextInfo, claimsSet); + } + if (authContextInfo.getGroupsPath() == null || roles == null) { + Object groupsClaim = claimsSet.getClaimValue(Claims.groups.name()); + if (groupsClaim instanceof String) { + roles = splitStringClaimValue(groupsClaim.toString(), authContextInfo); + } else if (groupsClaim != null && !(groupsClaim instanceof Map)) { + @SuppressWarnings("") + List rolesFromGroupsClaim = List.class.cast(groupsClaim); + roles = rolesFromGroupsClaim; + } } + if (roles == null || roles.isEmpty()) { + final String defaultGroupsClaim = authContextInfo.getDefaultGroupsClaim(); + if (defaultGroupsClaim != null) { + roles = Collections.singletonList(defaultGroupsClaim); + } + } + claimsSet.setClaim(Claims.groups.name(), roles); // Process the rolesMapping claim if (claimsSet.hasClaim(ROLE_MAPPINGS)) { @@ -79,29 +92,23 @@ private static String findSubject(JWTAuthContextInfo authContextInfo, JwtClaims } private static List findGroups(JWTAuthContextInfo authContextInfo, JwtClaims claimsSet) { - if (authContextInfo.getGroupsPath() != null) { - final String[] pathSegments = splitClaimPath(authContextInfo.getGroupsPath()); - Object claimValue = findClaimValue(authContextInfo.getGroupsPath(), claimsSet.getClaimsMap(), pathSegments, 0); + final String[] pathSegments = splitClaimPath(authContextInfo.getGroupsPath()); + Object claimValue = findClaimValue(authContextInfo.getGroupsPath(), claimsSet.getClaimsMap(), pathSegments, 0); - if (claimValue instanceof List) { - @SuppressWarnings("unchecked") - List groups = List.class.cast(claimValue); - // Force a check that a list contains the string values only - try { - return Arrays.asList(groups.toArray(new String[] {})); - } catch (ArrayStoreException ex) { - PrincipalLogging.log.claimAtPathIsNotAnArrayOfStrings(authContextInfo.getGroupsPath()); - } - } else if (claimValue instanceof String) { - return splitStringClaimValue(claimValue.toString(), authContextInfo); - } else { - PrincipalLogging.log.claimAtPathIsNeitherAnArrayOfStringsNorString(authContextInfo.getGroupsPath()); + if (claimValue instanceof List) { + @SuppressWarnings("unchecked") + List groups = List.class.cast(claimValue); + // Force a check that a list contains the string values only + try { + return Arrays.asList(groups.toArray(new String[] {})); + } catch (ArrayStoreException ex) { + PrincipalLogging.log.claimAtPathIsNotAnArrayOfStrings(authContextInfo.getGroupsPath()); } + } else if (claimValue instanceof String) { + return splitStringClaimValue(claimValue.toString(), authContextInfo); + } else { + PrincipalLogging.log.claimAtPathIsNeitherAnArrayOfStringsNorString(authContextInfo.getGroupsPath()); } - if (authContextInfo.getDefaultGroupsClaim() != null) { - return Collections.singletonList(authContextInfo.getDefaultGroupsClaim()); - } - return null; } diff --git a/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/PrincipalUtilsTest.java b/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/PrincipalUtilsTest.java new file mode 100644 index 00000000..11d73812 --- /dev/null +++ b/implementation/jwt-auth/src/test/java/io/smallrye/jwt/auth/principal/PrincipalUtilsTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2019 Red Hat, Inc, and individual contributors. + * + * Licensed 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 io.smallrye.jwt.auth.principal; + +import static org.junit.jupiter.api.Assertions.assertIterableEquals; + +import java.util.List; + +import org.eclipse.microprofile.jwt.Claims; +import org.jose4j.jwt.JwtClaims; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opentest4j.AssertionFailedError; + +@ExtendWith(MockitoExtension.class) +class PrincipalUtilsTest { + + private static final List rolesInGroupsClaim = List.of("group1", "group2"); + private static final String rolesInGroupsClaimAsString = "group1 group2"; + private static final String defaultRole = "default1"; + private static final List rolesInDefaultGroup = List.of(defaultRole); + private static final List rolesInCustomGroupsClaim = List.of("custom1", "custom2"); + private static final String rolesInCustomGroupsClaimAsString = "custom1 custom2"; + + private static final List tests = List.of( + new TestData("group claim is set, custom groups are not set", + rolesInGroupsClaim, + rolesInGroupsClaim, null, false, null), + new TestData("group claim is set, custom groups are not set, default role is set", + rolesInGroupsClaim, + rolesInGroupsClaim, null, false, defaultRole), + + new TestData("group claim is set, custom groups are set", + rolesInCustomGroupsClaim, + rolesInGroupsClaim, rolesInCustomGroupsClaim, true, null), + new TestData("group claim is set, custom groups are set, default role is set", + rolesInCustomGroupsClaim, + rolesInGroupsClaim, rolesInCustomGroupsClaim, true, defaultRole), + + new TestData("group claim is set, custom groups are set but empty", + rolesInGroupsClaim, + rolesInGroupsClaim, null, true, null), + new TestData("group claim is set, custom groups are set but empty, default role is set", + rolesInGroupsClaim, + rolesInGroupsClaim, null, true, defaultRole), + + new TestData("group claim is empty, custom groups are set", + rolesInCustomGroupsClaim, + null, rolesInCustomGroupsClaim, true, null), + new TestData("group claim is empty, custom groups are set, and default role is set", + rolesInCustomGroupsClaim, + null, rolesInCustomGroupsClaim, true, defaultRole), + + new TestData("group claim is empty, custom groups are not set", + null, + null, null, false, null), + new TestData("group claim is empty, custom groups are not set, default role is set", + rolesInDefaultGroup, + null, null, false, defaultRole), + + new TestData("group claim is empty, custom groups are set but empty", + null, + null, null, true, null), + new TestData("group claim is empty, custom groups are set but empty, default role is set", + rolesInDefaultGroup, + null, null, true, defaultRole), + + new TestData("group claim is set as string, custom groups are not set", + rolesInGroupsClaim, + rolesInGroupsClaimAsString, null, false, null), + new TestData("group claim is empty, custom groups are set as string", + rolesInCustomGroupsClaim, + null, rolesInCustomGroupsClaimAsString, true, null)); + + @Test + void testGroupsClaimSettings() throws Exception { + for (TestData td : tests) { + JwtClaims claimSet = td.getClaimSet(); + PrincipalUtils.setClaims(claimSet, td.getToken(), td.getAuthContextInfo()); + + @SuppressWarnings("unchecked") + List actualRoles = List.class.cast(claimSet.getClaimValue(Claims.groups.name())); + try { + assertIterableEquals(td.getExpectedRoles(), actualRoles); + } catch (AssertionFailedError e) { + throw new AssertionFailedError(td.getName(), e); + } + } + } + + private static class TestData { + + private static final String CUSTOM_GROUPS_PATH = "testroles"; + + private final String name; + private final List expectedRoles; + private final Object rolesInGroupsClaim; + private final Object rolesInCustomClaim; + private final boolean setCustomGroupsPath; + private final String defaultGroupsClaim; + + public TestData(String name, List expectedRoles, Object rolesInGroupsClaim, Object rolesInCustomClaim, + boolean setCustomGroupsPath, String defaultGroupsClaim) { + this.name = name; + this.expectedRoles = expectedRoles; + this.rolesInGroupsClaim = rolesInGroupsClaim; + this.rolesInCustomClaim = rolesInCustomClaim; + this.setCustomGroupsPath = setCustomGroupsPath; + this.defaultGroupsClaim = defaultGroupsClaim; + } + + public String getName() { + return name; + } + + public List getExpectedRoles() { + return expectedRoles; + } + + public JwtClaims getClaimSet() { + JwtClaims claimSet = new JwtClaims(); + if (rolesInGroupsClaim != null) { + claimSet.setClaim(Claims.groups.name(), rolesInGroupsClaim); + } + if (rolesInCustomClaim != null) { + claimSet.setClaim(CUSTOM_GROUPS_PATH, rolesInCustomClaim); + } + return claimSet; + } + + public String getToken() { + return "test.token.signature"; + } + + public JWTAuthContextInfo getAuthContextInfo() { + JWTAuthContextInfo authContextInfo = new JWTAuthContextInfo(); + if (setCustomGroupsPath) { + authContextInfo.setGroupsPath(CUSTOM_GROUPS_PATH); + } + authContextInfo.setDefaultGroupsClaim(defaultGroupsClaim); + return authContextInfo; + } + + } + +}