Skip to content

Commit

Permalink
Jwt multi admin (geoserver#7610)
Browse files Browse the repository at this point in the history
* Add JWT Header support for multiple ADMIN/ROLE_ADMINISTRATOR support

* better javascript handling for when creating a new jwt headers filter

* doc change for role converter

* fix bug when json path doesn't exist

* delete old src/ dir

* afabiani review changes - (c) header, more specific import, UI: Role Source enum

---------

Co-authored-by: david blasby <[email protected]>
  • Loading branch information
davidblasby and david-blasby authored May 13, 2024
1 parent c98f9c0 commit b692158
Show file tree
Hide file tree
Showing 43 changed files with 126 additions and 3,179 deletions.
6 changes: 6 additions & 0 deletions doc/en/user/source/community/jwt-headers/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ For example, a conversion map like `GeoserverAdministrator=ROLE_ADMINISTRATOR` w

In our example, the user has two roles "GeoserverAdministrator" and "GeonetworkAdministrator". If the "Only allow External Roles that are explicitly named above" is checked, then GeoServer will only see the "ROLE_ADMINISTRATOR" role. If unchecked, it will see "ROLE_ADMINISTRATOR" and "GeonetworkAdministrator". In neither case will it see the converted "GeoserverAdministrator" roles.

You can also have multiple GeoServer roles from one external (OIDC) role. For example, this role conversion:

`GeoserverAdministrator=ROLE_ADMINISTRATOR;GeoserverAdministrator=ADMIN`

Will give users with the OIDC role `GeoserverAdministrator` two GeoServer roles - `ROLE_ADMINISTRATOR` and `ADMIN`.


JWT Validation
^^^^^^^^^^^^^^
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ public SecurityConfig clone(boolean allowEnvParametrization) {
/** what formats we support for roles in the header. */
public enum JWTHeaderRoleSource implements RoleSource {
JSON,
JWT;
JWT,

// From: PreAuthenticatedUserNameFilterConfig
Header,
UserGroupService,
RoleService;

@Override
public boolean equals(RoleSource other) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,21 @@
visibilityDiv.style.display = "none";
}

// When the page is loaded, we hide the username "json path" input if its not needed.
window.addEventListener('load', function () {
function reset() {
usernameFormatChanged();
showTokenValidationChanged();
toggleVisible(document.getElementById('validateTokenSignature'),'validateTokenSignatureURLDiv');
toggleVisible(document.getElementById('validateTokenAgainstURL'),'validateTokenAgainstURLDiv');
toggleVisible(document.getElementById('validateTokenAudience'),'validateTokenAudienceDiv');
}


// When the page is loaded, we hide the username "json path" input if its not needed.
window.addEventListener('load', function () {
reset();
});

// when creating a new jwt headers filter, we need to "kick" it.
setTimeout(reset,100);
</script>
</wicket:head>
<wicket:extend>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.jwtheaders;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JwtConfiguration implements Serializable {
Expand Down Expand Up @@ -58,8 +64,8 @@ public class JwtConfiguration implements Serializable {
// convert string of the form:
// "externalRoleName1=GeoServerRoleName1;externalRoleName2=GeoServerRoleName2"
// To a Map<String,String>
public Map<String, String> getRoleConverterAsMap() {
Map<String, String> result = new HashMap<>();
public Map<String, List<String>> getRoleConverterAsMap() {
Map<String, List<String>> result = new HashMap<>();

if (roleConverterString == null || roleConverterString.isBlank()) return result; // empty

Expand All @@ -72,7 +78,13 @@ public Map<String, String> getRoleConverterAsMap() {
String key = goodCharacters(keyValue[0]);
String val = goodCharacters(keyValue[1]);
if (key.isBlank() || val.isBlank()) continue;
result.put(key, val);
if (!result.containsKey(key)) {
var list = new ArrayList<String>();
list.add(val);
result.put(key, list);
} else {
result.get(key).add(val);
}
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
public class RoleConverter {

Map<String, String> conversionMap;
Map<String, List<String>> conversionMap;

boolean externalNameMustBeListed;

Expand All @@ -39,11 +39,11 @@ public List<String> convert(List<String> externalRoles) {
if (externalRoles == null) return result; // empty

for (String externalRole : externalRoles) {
String gsRole = conversionMap.get(externalRole);
List<String> gsRole = conversionMap.get(externalRole);
if (gsRole == null && !externalNameMustBeListed) {
result.add(externalRole);
} else if (gsRole != null) {
result.add(gsRole);
result.addAll(gsRole);
}
}
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public TokenValidator(JwtConfiguration config) {

public void validate(String accessToken) throws Exception {

accessToken = accessToken.replaceFirst("^Bearer", "");
accessToken = accessToken.replaceFirst("^bearer", "");
accessToken = accessToken.trim();

if (!jwtHeadersConfig.isValidateToken()) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ public static Object getClaim(Map<String, Object> map, String path) {
// if this is trivial (single item in pathList), return the value.
// otherwise, go into the map one level (pathList[0]) and recurse on the result.
private static Object getClaim(Map<String, Object> map, List<String> pathList) {
if (pathList.size() == 1) return map.get(pathList.get(0));
if (map == null) {
return null;
}
if (pathList.size() == 1) {
return map.get(pathList.get(0));
}

String first = pathList.get(0);
pathList.remove(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ public void testSimpleJwt() throws ParseException {
Assert.assertEquals("GeoserverAdministrator", roles.get(0));
}

/**
* Test Tokens that start with "Bearer ".
*
* @throws ParseException
*/
@Test
public void testSimpleJwtBearer() throws ParseException {
String accessToken =
"Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItWEdld190TnFwaWRrYTl2QXNJel82WEQtdnJmZDVyMlNWTWkwcWMyR1lNIn0.eyJleHAiOjE3MDcxNTMxNDYsImlhdCI6MTcwNzE1Mjg0NiwiYXV0aF90aW1lIjoxNzA3MTUyNjQ1LCJqdGkiOiJlMzhjY2ZmYy0zMWNjLTQ0NmEtYmU1Yy04MjliNDE0NTkyZmQiLCJpc3MiOiJodHRwczovL2xvZ2luLWxpdmUtZGV2Lmdlb2NhdC5saXZlL3JlYWxtcy9kYXZlLXRlc3QyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImVhMzNlM2NjLWYwZTEtNDIxOC04OWNiLThkNDhjMjdlZWUzZCIsInR5cCI6IkJlYXJlciIsImF6cCI6ImxpdmUta2V5MiIsIm5vbmNlIjoiQldzc2M3cTBKZ0tHZC1OdFc1QlFhVlROMkhSa25LQmVIY0ZMTHZ5OXpYSSIsInNlc3Npb25fc3RhdGUiOiIxY2FiZmU1NC1lOWU0LTRjMmMtODQwNy03NTZiMjczZmFmZmIiLCJhY3IiOiIwIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZGF2ZS10ZXN0MiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJsaXZlLWtleTIiOnsicm9sZXMiOlsiR2Vvc2VydmVyQWRtaW5pc3RyYXRvciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgcGhvbmUgb2ZmbGluZV9hY2Nlc3MgbWljcm9wcm9maWxlLWp3dCBwcm9maWxlIGFkZHJlc3MgZW1haWwiLCJzaWQiOiIxY2FiZmU1NC1lOWU0LTRjMmMtODQwNy03NTZiMjczZmFmZmIiLCJ1cG4iOiJkYXZpZC5ibGFzYnlAZ2VvY2F0Lm5ldCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYWRkcmVzcyI6e30sIm5hbWUiOiJkYXZpZCBibGFzYnkiLCJncm91cHMiOlsiZGVmYXVsdC1yb2xlcy1kYXZlLXRlc3QyIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJkYXZpZC5ibGFzYnlAZ2VvY2F0Lm5ldCIsImdpdmVuX25hbWUiOiJkYXZpZCIsImZhbWlseV9uYW1lIjoiYmxhc2J5IiwiZW1haWwiOiJkYXZpZC5ibGFzYnlAZ2VvY2F0Lm5ldCJ9.fHzXd7oISnqWb09ah9wikfP2UOBeiOA3vd_aDg3Bw-xcfv9aD3CWhAK5FUDPYSPyj4whAcknZbUgUzcm0qkaI8V_aS65F3Fug4jt4nC9YPL4zMSJ5an4Dp6jlQ3OQhrKFn4FwaoW61ndMmScsZZWEQyj6gzHnn5cknqySB26tVydT6q57iTO7KQFcXRdbXd6GWIoFGS-ud9XzxQMUdNfYmsDD7e6hoWhe9PJD9Zq4KT6JN13hUU4Dos-Z5SBHjRa6ieHoOe9gqkjKyA1jT1NU42Nqr-mTV-ql22nAoXuplpvOYc5-09-KDDzSDuVKFwLCNMN3ZyRF1wWuydJeU-gOQ";

List<String> roles =
getExtractor(JWT.toString(), "", "resource_access.live-key2.roles")
.getRoles(accessToken).stream()
.collect(Collectors.toList());
Assert.assertEquals(1, roles.size());
Assert.assertEquals("GeoserverAdministrator", roles.get(0));
}

@Test
public void testSimpleJson() throws ParseException {
String json =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void testParseNull() {
JwtConfiguration config = new JwtConfiguration();

config.setRoleConverterString(null);
Map<String, String> map = config.getRoleConverterAsMap();
Map<String, List<String>> map = config.getRoleConverterAsMap();
Assert.assertEquals(0, map.size());

config.setRoleConverterString("");
Expand All @@ -50,18 +50,18 @@ public void testParseSimple() {
JwtConfiguration config = new JwtConfiguration();

config.setRoleConverterString("a=b");
Map<String, String> map = config.getRoleConverterAsMap();
Map<String, List<String>> map = config.getRoleConverterAsMap();
Assert.assertEquals(1, map.size());
Assert.assertTrue(map.containsKey("a"));
Assert.assertEquals("b", map.get("a"));
Assert.assertEquals(Arrays.asList("b"), map.get("a"));

config.setRoleConverterString("a=b;c=d");
map = config.getRoleConverterAsMap();
Assert.assertEquals(2, map.size());
Assert.assertTrue(map.containsKey("a"));
Assert.assertEquals("b", map.get("a"));
Assert.assertEquals(Arrays.asList("b"), map.get("a"));
Assert.assertTrue(map.containsKey("c"));
Assert.assertEquals("d", map.get("c"));
Assert.assertEquals(Arrays.asList("d"), map.get("c"));
}

/**
Expand All @@ -75,24 +75,24 @@ public void testParseBad() {

// bad format
config.setRoleConverterString("a=b;c=;d");
Map<String, String> map = config.getRoleConverterAsMap();
Map<String, List<String>> map = config.getRoleConverterAsMap();
Assert.assertEquals(1, map.size());
Assert.assertTrue(map.containsKey("a"));
Assert.assertEquals("b", map.get("a"));
Assert.assertEquals(Arrays.asList("b"), map.get("a"));

// bad chars
config.setRoleConverterString("a= b** ;c=**;d");
map = config.getRoleConverterAsMap();
Assert.assertEquals(1, map.size());
Assert.assertTrue(map.containsKey("a"));
Assert.assertEquals("b", map.get("a"));
Assert.assertEquals(Arrays.asList("b"), map.get("a"));

// removes html tags
config.setRoleConverterString("a= <script> ;c=**;d");
map = config.getRoleConverterAsMap();
Assert.assertEquals(1, map.size());
Assert.assertTrue(map.containsKey("a"));
Assert.assertEquals("script", map.get("a"));
Assert.assertEquals(Arrays.asList("script"), map.get("a"));
}

/** Tests simple conversion, with setOnlyExternalListedRoles(false); */
Expand Down Expand Up @@ -133,4 +133,23 @@ public void testConversionOnlyMapped() {

Assert.assertEquals("d", internalRoles.get(0));
}

/**
* Test for creating multiple roles. purpose - make sure that a user can get multiple GS roles
* from a single OIDC role.
*/
@Test
public void testMultipleRoles() {
JwtConfiguration config = new JwtConfiguration();
config.setRoleConverterString("a=ROLE_ADMINISTRATOR;a=ADMIN");
config.setOnlyExternalListedRoles(true);

RoleConverter roleConverter = new RoleConverter(config);
List<String> externalRoles = Arrays.asList("a");
List<String> internalRoles = roleConverter.convert(externalRoles);
Assert.assertEquals(2, internalRoles.size());

Assert.assertEquals("ROLE_ADMINISTRATOR", internalRoles.get(0));
Assert.assertEquals("ADMIN", internalRoles.get(1));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ public void testSimpleJwt() throws ParseException {
Assert.assertEquals("[email protected]", username);
}

String json =
"{\"exp\":1707155912,\"iat\":1707155612,\"jti\":\"888715ae-a79d-4633-83e5-9b97dee02bbc\",\"iss\":\"https://login-live-dev.geocat.live/realms/dave-test2\",\"aud\":\"account\",\"sub\":\"ea33e3cc-f0e1-4218-89cb-8d48c27eee3d\",\"typ\":\"Bearer\",\"azp\":\"live-key2\",\"session_state\":\"ae7796fa-b374-4754-a294-e0eb834b23b5\",\"acr\":\"1\",\"realm_access\":{\"roles\":[\"default-roles-dave-test2\",\"offline_access\",\"uma_authorization\"]},\"resource_access\":{\"live-key2\":{\"roles\":[\"GeoserverAdministrator\"]},\"account\":{\"roles\":[\"manage-account\",\"manage-account-links\",\"view-profile\"]}},\"scope\":\"openidprofileemail\",\"sid\":\"ae7796fa-b374-4754-a294-e0eb834b23b5\",\"email_verified\":false,\"name\":\"davidblasby\",\"preferred_username\":\"[email protected]\",\"given_name\":\"david\",\"family_name\":\"blasby\",\"email\":\"[email protected]\"}";

@Test
public void testSimpleJson() throws ParseException {
String json =
"{\"exp\":1707155912,\"iat\":1707155612,\"jti\":\"888715ae-a79d-4633-83e5-9b97dee02bbc\",\"iss\":\"https://login-live-dev.geocat.live/realms/dave-test2\",\"aud\":\"account\",\"sub\":\"ea33e3cc-f0e1-4218-89cb-8d48c27eee3d\",\"typ\":\"Bearer\",\"azp\":\"live-key2\",\"session_state\":\"ae7796fa-b374-4754-a294-e0eb834b23b5\",\"acr\":\"1\",\"realm_access\":{\"roles\":[\"default-roles-dave-test2\",\"offline_access\",\"uma_authorization\"]},\"resource_access\":{\"live-key2\":{\"roles\":[\"GeoserverAdministrator\"]},\"account\":{\"roles\":[\"manage-account\",\"manage-account-links\",\"view-profile\"]}},\"scope\":\"openidprofileemail\",\"sid\":\"ae7796fa-b374-4754-a294-e0eb834b23b5\",\"email_verified\":false,\"name\":\"davidblasby\",\"preferred_username\":\"[email protected]\",\"given_name\":\"david\",\"family_name\":\"blasby\",\"email\":\"[email protected]\"}";
String username =
getExtractor(JwtConfiguration.UserNameHeaderFormat.JSON, "preferred_username")
.extractUserName(json);
Expand All @@ -49,4 +50,33 @@ public void testSimpleString() throws ParseException {
.extractUserName(json);
Assert.assertEquals("[email protected]", username);
}

@Test
public void testNonExistentClaim() {
String claimValue =
getExtractor(JwtConfiguration.UserNameHeaderFormat.JSON, "notThere")
.extractUserName(json);
Assert.assertNull(claimValue);

claimValue =
getExtractor(
JwtConfiguration.UserNameHeaderFormat.JSON,
"resource_access.notThere.abc")
.extractUserName(json);
Assert.assertNull(claimValue);

claimValue =
getExtractor(
JwtConfiguration.UserNameHeaderFormat.JSON,
"resource_access.live-key2.notThere")
.extractUserName(json);
Assert.assertNull(claimValue);

claimValue =
getExtractor(
JwtConfiguration.UserNameHeaderFormat.JSON,
"resource_access.live-key2.roles.notThere")
.extractUserName(json);
Assert.assertNull(claimValue);
}
}

This file was deleted.

Loading

0 comments on commit b692158

Please sign in to comment.