Skip to content

Commit

Permalink
JWT Headers security module (#7899)
Browse files Browse the repository at this point in the history
* JWT Headers security module

* changes from jose's review

* make sure that token validation is being done

* allow two jwt-header auth filters at the same time + configuration

* fix config problem, remove some unnecessary code

* added support for easily turning on/off UpdateProfile and UpdateGroup + changed typo

* update jwt shared library (gn+gs) dependency to 2.27 to include GS PR#8102

---------

Co-authored-by: david.blasby <[email protected]>
  • Loading branch information
davidblasby and david-blasby authored Dec 11, 2024
1 parent 36ce40b commit 4fbdefd
Show file tree
Hide file tree
Showing 18 changed files with 2,414 additions and 0 deletions.
11 changes: 11 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
<name>GeoNetwork core</name>

<dependencies>

<dependency>
<groupId>org.geoserver.community.jwt-headers</groupId>
<artifactId>jwt-headers-util</artifactId>
<version>2.27-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>net.objecthunter</groupId>
<artifactId>exp4j</artifactId>
Expand Down Expand Up @@ -304,6 +311,10 @@
<groupId>org.geotools</groupId>
<artifactId>gt-geojson</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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: [email protected]
*/

package org.fao.geonet.kernel.security.jwtheaders;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;


/**
* This handles the JWT-Headers authentication filter. It's based on the Shibboleth filter.
*/
public class JwtHeadersAuthFilter extends GenericFilterBean {

@Autowired
public JwtHeadersUserUtil jwtHeadersUserUtil;

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;
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
var existingAuth = SecurityContextHolder.getContext().getAuthentication();
HttpServletRequest request = (HttpServletRequest) servletRequest;


var config = jwtHeadersConfiguration.getJwtConfiguration();

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
&& ((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);
filterChain.doFilter(servletRequest, servletResponse);
return;
}
}


if (user == null) {
filterChain.doFilter(servletRequest, servletResponse);
return; // no valid user in header
}

//we have a valid user in the headers

//existing user is the same user as the request
if (existingAuth != null && existingAuth.getName().equals(user.getUsername())) {
filterChain.doFilter(servletRequest, servletResponse);
return; // abort early - no need to do an expensive login. Use the existing one.
}

//existing user isnt the same user as the request
if (existingAuth != null && !existingAuth.getName().equals(user.getUsername())) {
//in this case there are two auth's - the existing one (likely from JSESSION)
//and one coming in from the JWT headers. In this case, we kill the other login
//and make a new one.
logout(request);
}

var userDetails = jwtHeadersUserUtil.getUser(user, jwtHeadersConfiguration);
if (userDetails != null) {
UsernamePasswordAuthenticationToken auth = new JwtHeadersUsernamePasswordAuthenticationToken(
filterId, userDetails, null, userDetails.getAuthorities());
auth.setDetails(userDetails);
SecurityContextHolder.getContext().setAuthentication(auth);
}

filterChain.doFilter(servletRequest, servletResponse);
}

/**
* handle a logout - clear out the security context, and invalidate the session
*
* @param request
* @throws ServletException
*/
public void logout(HttpServletRequest request) throws ServletException {
request.logout();//dont think this does anything in GN
SecurityContextHolder.getContext().setAuthentication(null);
request.getSession().invalidate();
}

}


Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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: [email protected]
*/

package org.fao.geonet.kernel.security.jwtheaders;

import org.fao.geonet.kernel.security.SecurityProviderConfiguration;
import org.geoserver.security.jwtheaders.JwtConfiguration;

/**
* 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.
* <p>
* 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 {


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;
protected JwtConfiguration jwtConfiguration;

//shared JwtHeadersSecurityConfig object
JwtHeadersSecurityConfig securityConfig;

// getters/setters

public JwtHeadersConfiguration(JwtHeadersSecurityConfig securityConfig) {
this.securityConfig = securityConfig;
jwtConfiguration = new JwtConfiguration();
}

public boolean isUpdateProfile() {
return securityConfig.isUpdateProfile();
}

public void setUpdateProfile(boolean updateProfile) {
securityConfig.setUpdateProfile(updateProfile);
}

public boolean isUpdateGroup() {
return securityConfig.isUpdateGroup();
}


//---- abstract class methods

public void setUpdateGroup(boolean updateGroup) {
securityConfig.setUpdateGroup(updateGroup);
}

public String getLoginType() {
return securityConfig.getLoginType();
}


public String getSecurityProvider() {
return securityConfig.getSecurityProvider();
}


public boolean isUserProfileUpdateEnabled() {
return securityConfig.isUserProfileUpdateEnabled();
}

//========================================================================

// @Override
public boolean isUserGroupUpdateEnabled() {
return securityConfig.isUserGroupUpdateEnabled();
}

public org.geoserver.security.jwtheaders.JwtConfiguration getJwtConfiguration() {
return jwtConfiguration;
}

public void setJwtConfiguration(
org.geoserver.security.jwtheaders.JwtConfiguration jwtConfiguration) {
this.jwtConfiguration = jwtConfiguration;
}

}
Original file line number Diff line number Diff line change
@@ -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: [email protected]
*/
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;
}

}
Loading

0 comments on commit 4fbdefd

Please sign in to comment.