Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for multiple custom claim paths to find roles in oidc token #23139

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/main/asciidoc/security-openid-connect.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ The default tenant's `OidcConfigurationMetadata` is injected if the endpoint is

SecurityIdentity roles can be mapped from the verified JWT access tokens as follows:

* If `quarkus.oidc.roles.role-claim-path` property is set and a matching array or string claim is found then the roles are extracted from this claim.
* If `quarkus.oidc.roles.role-claim-path` property is set and matching array or string claims are found then the roles are extracted from these claims.
For example, `customroles`, `customroles/array`, `scope`, `"http://namespace-qualified-custom-claim"/roles`, `"http://namespace-qualified-roles"`, etc.
* If `groups` claim is available then its value is used
* If `realm_access/roles` or `resource_access/client_id/roles` (where `client_id` is the value of the `quarkus.oidc.client-id` property) claim is available then its value is used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,28 +349,29 @@ public Logout getLogout() {
@ConfigGroup
public static class Roles {

public static Roles fromClaimPath(String path) {
public static Roles fromClaimPath(List<String> path) {
return fromClaimPathAndSeparator(path, null);
}

public static Roles fromClaimPathAndSeparator(String path, String sep) {
public static Roles fromClaimPathAndSeparator(List<String> path, String sep) {
Roles roles = new Roles();
roles.roleClaimPath = Optional.ofNullable(path);
roles.roleClaimSeparator = Optional.ofNullable(sep);
return roles;
}

/**
* Path to the claim containing an array of groups. It starts from the top level JWT JSON object and
* can contain multiple segments where each segment represents a JSON object name only, example: "realm/groups".
* Use double quotes with the namespace qualified claim names.
* This property can be used if a token has no 'groups' claim but has the groups set in a different claim.
* List of paths to claims containing an array of groups. Each path starts from the top level JWT JSON object
* and can contain multiple segments where each segment represents a JSON object name only,
* example: "realm/groups". Use double quotes with the namespace qualified claim names.
* This property can be used if a token has no 'groups' claim but has the groups set in one or more different
* claims.
*/
@ConfigItem
public Optional<String> roleClaimPath = Optional.empty();
public Optional<List<String>> roleClaimPath = Optional.empty();
/**
* Separator for splitting a string which may contain multiple group values.
* It will only be used if the "role-claim-path" property points to a custom claim whose value is a string.
* It will only be used if the "role-claim-path" property points to one or more custom claims whose values are strings.
* A single space will be used by default because the standard 'scope' claim may contain a space separated sequence.
*/
@ConfigItem
Expand All @@ -382,11 +383,11 @@ public static Roles fromClaimPathAndSeparator(String path, String sep) {
@ConfigItem
public Optional<Source> source = Optional.empty();

public Optional<String> getRoleClaimPath() {
public Optional<List<String>> getRoleClaimPath() {
return roleClaimPath;
}

public void setRoleClaimPath(String roleClaimPath) {
public void setRoleClaimPath(List<String> roleClaimPath) {
this.roleClaimPath = Optional.of(roleClaimPath);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,13 @@ public static JsonObject decodeJwtHeaders(String jwt) {
}

public static List<String> findRoles(String clientId, OidcTenantConfig.Roles rolesConfig, JsonObject json) {
// If the user configured a specific path - check and enforce a claim at this path exists
// If the user configured specific paths - check and enforce the claims at these paths exist
if (rolesConfig.getRoleClaimPath().isPresent()) {
return findClaimWithRoles(rolesConfig, rolesConfig.getRoleClaimPath().get(), json);
List<String> roles = new LinkedList<>();
for (String roleClaimPath : rolesConfig.getRoleClaimPath().get()) {
roles.addAll(findClaimWithRoles(rolesConfig, roleClaimPath, json));
}
return roles;
}

// Check 'groups' next
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -317,17 +318,30 @@ public void testTokenWithGroups() throws Exception {

@Test
public void testTokenWithCustomRoles() throws Exception {
OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath("application_card/embedded/roles");
OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles
.fromClaimPath(Collections.singletonList("application_card/embedded/roles"));
List<String> roles = OidcUtils.findRoles(null, rolesCfg, read(getClass().getResourceAsStream("/tokenCustomPath.json")));
assertEquals(2, roles.size());
assertTrue(roles.contains("r1"));
assertTrue(roles.contains("r2"));
}

@Test
public void testTokenWithMultipleCustomRolePaths() throws Exception {
OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles
.fromClaimPath(List.of("application_card/embedded/roles", "application_card/embedded2/roles"));
List<String> roles = OidcUtils.findRoles(null, rolesCfg, read(getClass().getResourceAsStream("/tokenCustomPath.json")));
assertEquals(4, roles.size());
assertTrue(roles.contains("r1"));
assertTrue(roles.contains("r2"));
assertTrue(roles.contains("r5"));
assertTrue(roles.contains("r6"));
}

@Test
public void testTokenWithCustomNamespacedRoles() throws Exception {
OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles
.fromClaimPath("application_card/embedded/\"https://custom/roles\"");
.fromClaimPath(Collections.singletonList("application_card/embedded/\"https://custom/roles\""));
List<String> roles = OidcUtils.findRoles(null, rolesCfg, read(getClass().getResourceAsStream("/tokenCustomPath.json")));
assertEquals(2, roles.size());
assertTrue(roles.contains("r3"));
Expand All @@ -336,7 +350,7 @@ public void testTokenWithCustomNamespacedRoles() throws Exception {

@Test
public void testTokenWithScope() throws Exception {
OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath("scope");
OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath(Collections.singletonList("scope"));
List<String> roles = OidcUtils.findRoles(null, rolesCfg, read(getClass().getResourceAsStream("/tokenScope.json")));
assertEquals(2, roles.size());
assertTrue(roles.contains("s1"));
Expand All @@ -345,7 +359,8 @@ public void testTokenWithScope() throws Exception {

@Test
public void testTokenWithCustomScope() throws Exception {
OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPathAndSeparator("customScope", ",");
OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles
.fromClaimPathAndSeparator(Collections.singletonList("customScope"), ",");
List<String> roles = OidcUtils.findRoles(null, rolesCfg,
read(getClass().getResourceAsStream("/tokenCustomScope.json")));
assertEquals(2, roles.size());
Expand All @@ -355,7 +370,8 @@ public void testTokenWithCustomScope() throws Exception {

@Test
public void testTokenWithCustomRolesWrongPath() throws Exception {
OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles.fromClaimPath("application-card/embedded/roles");
OidcTenantConfig.Roles rolesCfg = OidcTenantConfig.Roles
.fromClaimPath(Collections.singletonList("application-card/embedded/roles"));
InputStream is = getClass().getResourceAsStream("/tokenCustomPath.json");
List<String> roles = OidcUtils.findRoles(null, rolesCfg, read(is));
assertEquals(0, roles.size());
Expand Down
26 changes: 16 additions & 10 deletions extensions/oidc/runtime/src/test/resources/tokenCustomPath.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,21 @@
"iat": 1311280970,
"auth_time": 1311280969,
"application_card": {
"embedded": {
"roles": [
"r1",
"r2"
],
"https://custom/roles": [
"r3",
"r4"
]
}
"embedded": {
"roles": [
"r1",
"r2"
],
"https://custom/roles": [
"r3",
"r4"
]
},
"embedded2": {
"roles": [
"r5",
"r6"
]
}
}
}