Skip to content

Commit

Permalink
allow two jwt-header auth filters at the same time + configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
david-blasby committed Apr 19, 2024
1 parent 4fe7ce6 commit ba3c2ff
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@

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

Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -98,17 +104,17 @@ 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);
}

/**
* handle a logout - clear out the security context, and invalidate the session
*
* @param request
* @throws ServletException
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <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 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).
Expand All @@ -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() {
Expand Down
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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -46,7 +45,6 @@

/**
* This class handles GeoNetwork related User (and Group/UserGroup) activities.
*
*/
public class JwtHeadersUserUtil {

Expand All @@ -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
* <p>
* 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) {
Expand All @@ -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).
*
* <p>
* isUpdateProfile/isUpdateGroup control if the DB is updated from the request Headers
*
* @param userFromDb
Expand Down Expand Up @@ -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
*
* <p>
* profile updating (in GN DB) is controlled by isUpdateGroup
* profileGroup updating (in GN DB) is controlled by isUpdateGroup
* <p>
* cf. updateGroups for how the profile/profileGroups are updated
*
* @param userFromHeaders
* @param configuration
* @return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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");

Expand Down
Loading

0 comments on commit ba3c2ff

Please sign in to comment.