diff --git a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersAuthFilter.java b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersAuthFilter.java index 249a18e351f..9a35028cf2c 100644 --- a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersAuthFilter.java +++ b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersAuthFilter.java @@ -38,7 +38,6 @@ /** * This handles the JWT-Headers authentication filter. It's based on the Shibboleth filter. - * */ public class JwtHeadersAuthFilter extends GenericFilterBean { @@ -47,6 +46,10 @@ public class JwtHeadersAuthFilter extends GenericFilterBean { JwtHeadersConfiguration jwtHeadersConfiguration; + //uniquely identify this authfilter + //this is need if there are >1 Jwt-Header filters active at the same time + String filterId = java.util.UUID.randomUUID().toString(); + public JwtHeadersAuthFilter(JwtHeadersConfiguration jwtHeadersConfiguration) { this.jwtHeadersConfiguration = jwtHeadersConfiguration; @@ -63,8 +66,11 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo var user = JwtHeadersTrivialUser.create(config, request); + //if request is already logged in by us (same filterId), but there aren't any Jwt-Headers attached + //then log them out. if (user == null && existingAuth != null) { - if (existingAuth instanceof JwtHeadersUsernamePasswordAuthenticationToken) { + if (existingAuth instanceof JwtHeadersUsernamePasswordAuthenticationToken + && ((JwtHeadersUsernamePasswordAuthenticationToken) existingAuth).authFilterId.equals(filterId)) { //at this point, there isn't a JWT header, but there's an existing auth that was made by us (JWT header) // in this case, we need to log-off. They have a JSESSION auth that is no longer valid. logout(request); @@ -98,10 +104,9 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo var userDetails = jwtHeadersUserUtil.getUser(user, jwtHeadersConfiguration); if (userDetails != null) { UsernamePasswordAuthenticationToken auth = new JwtHeadersUsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); + filterId, userDetails, null, userDetails.getAuthorities()); auth.setDetails(userDetails); SecurityContextHolder.getContext().setAuthentication(auth); - } filterChain.doFilter(servletRequest, servletResponse); @@ -109,6 +114,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo /** * handle a logout - clear out the security context, and invalidate the session + * * @param request * @throws ServletException */ diff --git a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersConfiguration.java b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersConfiguration.java index c7642c57b3a..73d4fee4316 100644 --- a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersConfiguration.java +++ b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersConfiguration.java @@ -30,15 +30,14 @@ * configuration for the JWT Headers security filter. * See GN documentation. * This is based on GeoServer's JWT-Headers Module, so you can see there as well. - * + *
* This class handles the GN filter configuration details, and hands the actual configuration * for the filter to the JwtConfiguration class. This class is also used in Geoserver. - * */ -public class JwtHeadersConfiguration implements SecurityProviderConfiguration { +public class JwtHeadersConfiguration { - public LoginType loginType = LoginType.AUTOLOGIN; + public SecurityProviderConfiguration.LoginType loginType = SecurityProviderConfiguration.LoginType.AUTOLOGIN; /** * true -> update the DB with the information from OIDC (don't allow user to edit profile in the UI) * false -> don't update the DB (user must edit profile in UI). @@ -51,55 +50,54 @@ public class JwtHeadersConfiguration implements SecurityProviderConfiguration { public boolean updateGroup = true; protected JwtConfiguration jwtConfiguration; + //shared JwtHeadersSecurityConfig object + JwtHeadersSecurityConfig securityConfig; // getters/setters - - public JwtHeadersConfiguration() { + public JwtHeadersConfiguration(JwtHeadersSecurityConfig securityConfig) { + this.securityConfig = securityConfig; jwtConfiguration = new JwtConfiguration(); } public boolean isUpdateProfile() { - return updateProfile; + return securityConfig.isUpdateProfile(); } public void setUpdateProfile(boolean updateProfile) { - this.updateProfile = updateProfile; + securityConfig.setUpdateProfile(updateProfile); } public boolean isUpdateGroup() { - return updateGroup; + return securityConfig.isUpdateGroup(); } //---- abstract class methods public void setUpdateGroup(boolean updateGroup) { - this.updateGroup = updateGroup; + securityConfig.setUpdateGroup(updateGroup); } - @Override public String getLoginType() { - return loginType.toString(); + return securityConfig.getLoginType(); } - @Override + public String getSecurityProvider() { - return "JWT-HEADERS"; + return securityConfig.getSecurityProvider(); } - @Override + public boolean isUserProfileUpdateEnabled() { - // If updating profile from the security provider then disable the profile updates in the interface - return !updateProfile; + return securityConfig.isUserProfileUpdateEnabled(); } //======================================================================== - @Override + // @Override public boolean isUserGroupUpdateEnabled() { - // If updating group from the security provider then disable the group updates in the interface - return !updateGroup; + return securityConfig.isUserGroupUpdateEnabled(); } public org.geoserver.security.jwtheaders.JwtConfiguration getJwtConfiguration() { diff --git a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersSecurityConfig.java b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersSecurityConfig.java new file mode 100644 index 00000000000..3e311faaa3e --- /dev/null +++ b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersSecurityConfig.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2024 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ +package org.fao.geonet.kernel.security.jwtheaders; + +import org.fao.geonet.kernel.security.SecurityProviderConfiguration; + +/** + * GeoNetwork only allows one SecurityProviderConfiguration bean. + * In the jwt-headers-multi (2 auth filters) situation, we need to have a single SecurityProviderConfiguration. + * We, therefore, share a single one. + * This class is shared between all the JwtHeadersConfiguration objects. + */ +public class JwtHeadersSecurityConfig implements SecurityProviderConfiguration { + + + public SecurityProviderConfiguration.LoginType loginType = SecurityProviderConfiguration.LoginType.AUTOLOGIN; + /** + * true -> update the DB with the information from OIDC (don't allow user to edit profile in the UI) + * false -> don't update the DB (user must edit profile in UI). + */ + public boolean updateProfile = true; + /** + * true -> update the DB (user's group) with the information from OIDC (don't allow admin to edit user's groups in the UI) + * false -> don't update the DB (admin must edit groups in UI). + */ + public boolean updateGroup = true; + + + // getters/setters + + + public JwtHeadersSecurityConfig() { + + } + + public boolean isUpdateProfile() { + return updateProfile; + } + + public void setUpdateProfile(boolean updateProfile) { + this.updateProfile = updateProfile; + } + + public boolean isUpdateGroup() { + return updateGroup; + } + + + //---- abstract class methods + + public void setUpdateGroup(boolean updateGroup) { + this.updateGroup = updateGroup; + } + + //@Override + public String getLoginType() { + return loginType.toString(); + } + + // @Override + public String getSecurityProvider() { + return "JWT-HEADERS"; + } + + // @Override + public boolean isUserProfileUpdateEnabled() { + // If updating profile from the security provider then disable the profile updates in the interface + return !updateProfile; + } + + //======================================================================== + + // @Override + public boolean isUserGroupUpdateEnabled() { + // If updating group from the security provider then disable the group updates in the interface + return !updateGroup; + } + +} diff --git a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersTrivialUser.java b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersTrivialUser.java index c042c31938e..f6a3b021a03 100644 --- a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersTrivialUser.java +++ b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersTrivialUser.java @@ -85,10 +85,12 @@ public static JwtHeadersTrivialUser create(JwtConfiguration config, HttpServletR var tokenValidator = new TokenValidator(config); try { - tokenValidator.validate(userNameHeader); - } - catch (Exception e) { - throw new IOException("JWT Token is invalid",e); + var accessToken = userNameHeader.replaceFirst("^Bearer", ""); + accessToken = accessToken.replaceFirst("^bearer", ""); + accessToken = accessToken.trim(); + tokenValidator.validate(accessToken); + } catch (Exception e) { + throw new IOException("JWT Token is invalid", e); } //get roles from the headers (pay attention to config) diff --git a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersUserUtil.java b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersUserUtil.java index 40519830723..b5629c52183 100644 --- a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersUserUtil.java +++ b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersUserUtil.java @@ -36,7 +36,6 @@ import org.fao.geonet.repository.UserRepository; import org.fao.geonet.utils.Log; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import java.util.HashSet; @@ -46,7 +45,6 @@ /** * This class handles GeoNetwork related User (and Group/UserGroup) activities. - * */ public class JwtHeadersUserUtil { @@ -67,20 +65,20 @@ public class JwtHeadersUserUtil { /** * Gets a user. - * 1. if the user currently existing in the GN DB: - * - user is retrieved from the GN DB - * - if the profile/profileGroup update is true then the DB is updated with info from `userFromHeaders` - * - otherwise, the header roles are ignored and profile/profileGroups are taken from the GN DB - * - * 2. if the user doesn't existing in the DB: - * - user is created and saved to the DB - * - if the profile/profileGroup update is true then the DB is updated with info from `userFromHeaders` - * - otherwise, the header roles are ignored and profile/profileGroups are taken from the GN DB - * - NOTE: in this case, the user will not have any profile/profileGraoup - - * an admin will have to manually set them in GN GUI + * 1. if the user currently existing in the GN DB: + * - user is retrieved from the GN DB + * - if the profile/profileGroup update is true then the DB is updated with info from `userFromHeaders` + * - otherwise, the header roles are ignored and profile/profileGroups are taken from the GN DB + *
+ * 2. if the user doesn't existing in the DB: + * - user is created and saved to the DB + * - if the profile/profileGroup update is true then the DB is updated with info from `userFromHeaders` + * - otherwise, the header roles are ignored and profile/profileGroups are taken from the GN DB + * - NOTE: in this case, the user will not have any profile/profileGraoup - + * an admin will have to manually set them in GN GUI * * @param userFromHeaders This is user info supplied in the request headers - * @param configuration Configuration of the JWT Headers filter + * @param configuration Configuration of the JWT Headers filter * @return */ public User getUser(JwtHeadersTrivialUser userFromHeaders, JwtHeadersConfiguration configuration) { @@ -96,7 +94,7 @@ public User getUser(JwtHeadersTrivialUser userFromHeaders, JwtHeadersConfigurati /** * given an existing user (both from GN DB and from the Request Headers), * update roles (profile/profileGroups). - * + *
* isUpdateProfile/isUpdateGroup control if the DB is updated from the request Headers * * @param userFromDb @@ -130,11 +128,12 @@ public void injectRoles(User userFromDb, JwtHeadersTrivialUser userFromHeaders, /** * creates a new user based on what was in the request headers. - * - * profile updating (in GN DB) is controlled by isUpdateGroup - * profileGroup updating (in GN DB) is controlled by isUpdateGroup - * + *
+ * profile updating (in GN DB) is controlled by isUpdateGroup + * profileGroup updating (in GN DB) is controlled by isUpdateGroup + *
* cf. updateGroups for how the profile/profileGroups are updated
+ *
* @param userFromHeaders
* @param configuration
* @return
diff --git a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersUsernamePasswordAuthenticationToken.java b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersUsernamePasswordAuthenticationToken.java
index dc48e815bf9..1e83d15e2bb 100644
--- a/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersUsernamePasswordAuthenticationToken.java
+++ b/core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersUsernamePasswordAuthenticationToken.java
@@ -33,8 +33,11 @@
*/
public class JwtHeadersUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
+ //ID of the JwtHeaderAuthFilter that authenticated the user
+ String authFilterId;
- public JwtHeadersUsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection extends GrantedAuthority> authorities) {
+ public JwtHeadersUsernamePasswordAuthenticationToken(String authFilterId, Object principal, Object credentials, Collection extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
+ this.authFilterId = authFilterId;
}
}
diff --git a/core/src/test/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersIntegrationTest.java b/core/src/test/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersIntegrationTest.java
index 32755d5e946..f133167989c 100644
--- a/core/src/test/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersIntegrationTest.java
+++ b/core/src/test/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersIntegrationTest.java
@@ -65,7 +65,7 @@ public class JwtHeadersIntegrationTest {
* standard configuration for testing JSON
*/
public static JwtHeadersConfiguration getBasicConfig() {
- JwtHeadersConfiguration config = new JwtHeadersConfiguration();
+ JwtHeadersConfiguration config = new JwtHeadersConfiguration(new JwtHeadersSecurityConfig());
var jwtheadersConfiguration = config.getJwtConfiguration();
jwtheadersConfiguration.setUserNameHeaderAttributeName("OIDC_id_token_payload");
@@ -100,7 +100,7 @@ public static JwtHeadersConfiguration getBasicConfig() {
* standard configuration for testing JWT
*/
public static JwtHeadersConfiguration getBasicConfigJWT() {
- JwtHeadersConfiguration config = new JwtHeadersConfiguration();
+ JwtHeadersConfiguration config = new JwtHeadersConfiguration(new JwtHeadersSecurityConfig());
var jwtheadersConfiguration = config.getJwtConfiguration();
jwtheadersConfiguration.setUserNameHeaderAttributeName("TOKEN");
diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/authentication-mode.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/authentication-mode.md
index ece4cebbc50..7026e9c804b 100644
--- a/docs/manual/docs/administrator-guide/managing-users-and-groups/authentication-mode.md
+++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/authentication-mode.md
@@ -863,7 +863,7 @@ You must turn on JWT Header Support by setting the `GEONETWORK_SECURITY_TYPE` en
GEONETWORK_SECURITY_TYPE=jwt-headers
```
-Please see the files for more detailed configuration:
+Please see these files for more detailed configuration:
* `config-security-jwt-header.xml`
* `config-security-jwt-header-overrides.properties`
@@ -1038,13 +1038,33 @@ You can also extract roles from the Access Token in a similar manner - make sure
#### Using Headers or GeoNetwork Database for Profiles & Profile Groups
-Inside `JwtHeaderConfiguration`, use these values to determine where Profile and ProfileGroups come from.
+Inside `JwtHeaderSecurityConfig`, use these values to determine where Profile and ProfileGroups come from.
| Property | Meaning |
| ------------- | ------- |
|updateProfile| true -> update the DB with the information from OIDC (don't allow user to edit profile in the UI)
false -> don't update the DB (user must edit profile in UI). |
|updateGroup| true -> update the DB (user's group) with the information from OIDC (don't allow admin to edit user's groups in the UI)
false -> don't update the DB (admin must edit groups in UI).|
+### Using JWT Headers for both OIDC and OAUTH2 (Simultaneously)
+
+Using the above configuration, you can configure JWT Headers for either OIDC-based browser access (i.e. with Apache `mod_auth_openidc`) ***or*** for OAUTH2 based Bearer Token access. However, you cannot do both at the same time.
+
+To configure JWT Headers to simultaneously provide OIDC and OAUTH2 access, you can use the `jwt-headers-multi` configuration.
+
+To use this, set the `GEONETWORK_SECURITY_TYPE` to `jwt-headers-multi`
+
+```
+GEONETWORK_SECURITY_TYPE=jwt-headers-multi
+```
+
+Please see these files for more detailed configuration:
+* `config-security-jwt-header-multi.xml`
+* `config-security-jwt-header-multi-overrides.properties`
+
+This creates two JWT Header authentication filters for GeoNetwork - one for OIDC based Browser access, and one for OAUTH2 based Robot access.
+
+You configure each of these independently using the same environment variables described above.
+For the first filter, use the environment variables defined above (ie. `JWTHEADERS_UserNameFormat`). For the second filter, add a `2` at the end of the environment variable (i.e. `JWTHEADERS_UserNameFormat2`).
## Configuring EU Login {#authentication-ecas}
diff --git a/web/src/main/webapp/WEB-INF/config-security/config-security-jwt-headers-multi-overrides.properties b/web/src/main/webapp/WEB-INF/config-security/config-security-jwt-headers-multi-overrides.properties
new file mode 100644
index 00000000000..33681a08726
--- /dev/null
+++ b/web/src/main/webapp/WEB-INF/config-security/config-security-jwt-headers-multi-overrides.properties
@@ -0,0 +1,85 @@
+# Copyright (C) 2024 Food and Agriculture Organization of the
+# United Nations (FAO-UN), United Nations World Food Programme (WFP)
+# and United Nations Environment Programme (UNEP)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or (at
+# your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+#
+# Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
+# Rome - Italy. email: geonetwork@osgeo.org
+
+## This contains configuration options for TWO Jwt-Headers auth filters.
+
+
+## configuration for the FIRST filter
+
+
+jwtheadersConfiguration.JwtConfiguration.userNameHeaderAttributeName=${JWTHEADERS_UserNameHeaderFormat:OIDC_id_token_payload}
+jwtheadersConfiguration.JwtConfiguration.userNameFormatChoice=${JWTHEADERS_UserNameFormat:JSON}
+
+jwtheadersConfiguration.JwtConfiguration.UserNameJsonPath=${JWTHEADERS_UserNameJsonPath:preferred_username}
+
+jwtheadersConfiguration.JwtConfiguration.rolesJsonPath=${JWTHEADERS_RolesJsonPath:resource_access.live-key2.roles}
+jwtheadersConfiguration.JwtConfiguration.rolesHeaderName=${JWTHEADERS_RolesHeaderName:OIDC_id_token_payload}
+jwtheadersConfiguration.JwtConfiguration.jwtHeaderRoleSource=${JWTHEADERS_JwtHeaderRoleSource:JSON}
+
+jwtheadersConfiguration.JwtConfiguration.roleConverterString=${JWTHEADERS_RoleConverterString:"GeonetworkAdministrator=ADMINISTRATOR"}
+jwtheadersConfiguration.JwtConfiguration.onlyExternalListedRoles=${JWTHEADERS_OnlyExternalListedRoles:false}
+
+jwtheadersConfiguration.JwtConfiguration.validateToken=${JWTHEADERS_ValidateToken:false}
+
+jwtheadersConfiguration.JwtConfiguration.validateTokenExpiry=${JWTHEADERS_ValidateTokenExpiry:false}
+
+jwtheadersConfiguration.JwtConfiguration.validateTokenAgainstURL=${JWTHEADERS_ValidateTokenAgainstURL:true}
+jwtheadersConfiguration.JwtConfiguration.validateTokenAgainstURLEndpoint=${JWTHEADERS_ValidateTokenAgainstURLEndpoint:}
+jwtheadersConfiguration.JwtConfiguration.validateSubjectWithEndpoint=${JWTHEADERS_ValidateSubjectWithEndpoint:true}
+
+jwtheadersConfiguration.JwtConfiguration.validateTokenAudience=${JWTHEADERS_ValidateTokenAudience:true}
+jwtheadersConfiguration.JwtConfiguration.validateTokenAudienceClaimName=${JWTHEADERS_ValidateTokenAudienceClaimName:""}
+jwtheadersConfiguration.JwtConfiguration.validateTokenAudienceClaimValue=${JWTHEADERS_ValidateTokenAudienceClaimValue:""}
+
+jwtheadersConfiguration.JwtConfiguration.validateTokenSignature=${JWTHEADERS_ValidateTokenSignature:true}
+jwtheadersConfiguration.JwtConfiguration.validateTokenSignatureURL=${JWTHEADERS_ValidateTokenSignatureURL:""}
+
+
+## configuration for the SECOND filter. The only diffence between this and the above (first filter) is that
+## this is configuring the 2nd filter configuration (jwtheadersConfiguration2)
+## all the environment variables are the same EXCEPT they end in "2"
+
+jwtheadersConfiguration2.JwtConfiguration.userNameHeaderAttributeName=${JWTHEADERS_UserNameHeaderFormat2:OIDC_id_token_payload}
+jwtheadersConfiguration2.JwtConfiguration.userNameFormatChoice=${JWTHEADERS_UserNameFormat2:JSON}
+
+jwtheadersConfiguration2.JwtConfiguration.UserNameJsonPath=${JWTHEADERS_UserNameJsonPath2:preferred_username}
+
+jwtheadersConfiguration2.JwtConfiguration.rolesJsonPath=${JWTHEADERS_RolesJsonPath2:resource_access.live-key2.roles}
+jwtheadersConfiguration2.JwtConfiguration.rolesHeaderName=${JWTHEADERS_RolesHeaderName2:OIDC_id_token_payload}
+jwtheadersConfiguration2.JwtConfiguration.jwtHeaderRoleSource=${JWTHEADERS_JwtHeaderRoleSource2:JSON}
+
+jwtheadersConfiguration2.JwtConfiguration.roleConverterString=${JWTHEADERS_RoleConverterString2:"GeonetworkAdministrator=ADMINISTRATOR"}
+jwtheadersConfiguration2.JwtConfiguration.onlyExternalListedRoles=${JWTHEADERS_OnlyExternalListedRoles2:false}
+
+jwtheadersConfiguration2.JwtConfiguration.validateToken=${JWTHEADERS_ValidateToken2:false}
+
+jwtheadersConfiguration2.JwtConfiguration.validateTokenExpiry=${JWTHEADERS_ValidateTokenExpiry2:false}
+
+jwtheadersConfiguration2.JwtConfiguration.validateTokenAgainstURL=${JWTHEADERS_ValidateTokenAgainstURL2:true}
+jwtheadersConfiguration2.JwtConfiguration.validateTokenAgainstURLEndpoint=${JWTHEADERS_ValidateTokenAgainstURLEndpoint2:}
+jwtheadersConfiguration2.JwtConfiguration.validateSubjectWithEndpoint=${JWTHEADERS_ValidateSubjectWithEndpoint2:true}
+
+jwtheadersConfiguration2.JwtConfiguration.validateTokenAudience=${JWTHEADERS_ValidateTokenAudience2:true}
+jwtheadersConfiguration2.JwtConfiguration.validateTokenAudienceClaimName=${JWTHEADERS_ValidateTokenAudienceClaimName2:""}
+jwtheadersConfiguration2.JwtConfiguration.validateTokenAudienceClaimValue=${JWTHEADERS_ValidateTokenAudienceClaimValue2:""}
+
+jwtheadersConfiguration2.JwtConfiguration.validateTokenSignature=${JWTHEADERS_ValidateTokenSignature2:true}
+jwtheadersConfiguration2.JwtConfiguration.validateTokenSignatureURL=${JWTHEADERS_ValidateTokenSignatureURL2:""}
diff --git a/web/src/main/webapp/WEB-INF/config-security/config-security-jwt-headers-multi.xml b/web/src/main/webapp/WEB-INF/config-security/config-security-jwt-headers-multi.xml
new file mode 100644
index 00000000000..4f388202810
--- /dev/null
+++ b/web/src/main/webapp/WEB-INF/config-security/config-security-jwt-headers-multi.xml
@@ -0,0 +1,108 @@
+
+
+
+