Skip to content

Commit

Permalink
Support for multiple custom claim paths to find roles in oidc token
Browse files Browse the repository at this point in the history
  • Loading branch information
Markus-Schwer committed Jan 25, 2022
1 parent 03743fb commit 7870359
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 28 deletions.
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"
]
}
}
}

0 comments on commit 7870359

Please sign in to comment.