Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.2.x] [FIXES #409] Add Username Remapping & Uppercase Group Names Support in OAuth2 Authentication Filter #412

Open
wants to merge 1 commit into
base: 2.2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ public class OAuth2Configuration extends IdPConfiguration {
protected String revokeEndpoint;
protected boolean enableRedirectEntryPoint = false;
protected String principalKey;
private String uniqueUsername;
protected String rolesClaim;
protected String groupsClaim;
private boolean groupNamesUppercase = false;

/**
* Get an authentication entry point instance meant to handle redirect to the authorization
Expand Down Expand Up @@ -461,6 +463,25 @@ public void setPrincipalKey(String principalKey) {
this.principalKey = principalKey;
}

/**
* Whether we would like to use another claim to extract the actual "username" from the token
* claims.
*
* @return the unique username claim key.
*/
public String getUniqueUsername() {
return uniqueUsername;
}

/**
* Set the unique username claim key.
*
* @param uniqueUsername the unique username claim key.
*/
public void setUniqueUsername(String uniqueUsername) {
this.uniqueUsername = uniqueUsername;
}

/**
* The roles claim name.
*
Expand Down Expand Up @@ -493,6 +514,15 @@ public void setGroupsClaim(String groupsClaim) {
this.groupsClaim = groupsClaim;
}

public boolean isGroupNamesUppercase() {
return groupNamesUppercase;
}

public void setGroupNamesUppercase(boolean groupNamesUppercase) {
this.groupNamesUppercase = groupNamesUppercase;
}


/** Class the representing and endpoint with a HTTP method. */
public static class Endpoint {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
* <http://www.geo-solutions.it/>.
*
*/

package it.geosolutions.geostore.services.rest.security.oauth2;

import static com.google.common.collect.Lists.newArrayList;
Expand Down Expand Up @@ -442,17 +441,23 @@ protected List<String> parseScopes(String commaSeparatedScopes) {
*/
protected PreAuthenticatedAuthenticationToken createPreAuthentication(
String username, HttpServletRequest request, HttpServletResponse response) {
String idToken = OAuth2Utils.getIdToken();
JWTHelper jwtHelper = decodeAndValidateIdToken(idToken);
// Remap the username if the idToken is valid and the configuration is set
username = remapUsername(username, jwtHelper);
LOGGER.info("Retrieving user with authorities for username: {}", username);
User user = retrieveUserWithAuthorities(username, request, response);
if (user == null) return null;
if (user == null) {
LOGGER.error("User retrieval failed for username: {}", username);
return null;
}
SimpleGrantedAuthority authority =
new SimpleGrantedAuthority("ROLE_" + user.getRole().toString());
PreAuthenticatedAuthenticationToken authenticationToken =
new PreAuthenticatedAuthenticationToken(
user, null, Collections.singletonList(authority));
String idToken = OAuth2Utils.getIdToken();
if (user != null
&& (StringUtils.isNotBlank(configuration.getGroupsClaim())
|| StringUtils.isNotBlank(configuration.getRolesClaim()))) {
if (StringUtils.isNotBlank(configuration.getGroupsClaim())
|| StringUtils.isNotBlank(configuration.getRolesClaim())) {
addAuthoritiesFromToken(user, idToken);
}
OAuth2AccessToken accessToken = restTemplate.getOAuth2ClientContext().getAccessToken();
Expand All @@ -462,10 +467,63 @@ protected PreAuthenticatedAuthenticationToken createPreAuthentication(
}

/**
* Add authorities from the idToken claims if found.
* Decodes and validates the given idToken.
*
* <p>If the token is null or fails to decode, this method logs an appropriate message and
* returns null, causing the authentication to fall back to using the original username.
*
* @param idToken the idToken to decode.
* @return a {@link JWTHelper} instance if the token is valid, or null otherwise.
*/
protected JWTHelper decodeAndValidateIdToken(String idToken) {
if (idToken == null) {
LOGGER.warn("No idToken provided for decoding. Skipping username remapping.");
return null;
}
try {
// Optionally add additional validation logic for the token here (e.g. signature,
// expiration)
return new JWTHelper(idToken);
} catch (Exception e) {
LOGGER.error("Failed to decode or validate idToken: {}", idToken, e);
return null;
}
}

/**
* Remaps the provided username based on idToken claims if applicable.
*
* @param username the original username.
* @param jwtHelper the {@link JWTHelper} instance for decoding idToken claims, may be null.
* @return the remapped username if claims match; otherwise, the original username.
*/
private String remapUsername(String username, JWTHelper jwtHelper) {
if (jwtHelper != null
&& StringUtils.isNotBlank(configuration.getPrincipalKey())
&& StringUtils.isNotBlank(configuration.getUniqueUsername())) {
String principalClaim =
jwtHelper.getClaim(configuration.getPrincipalKey(), String.class);
if (StringUtils.isNotBlank(principalClaim)
&& StringUtils.equals(username, principalClaim)) {
String uniqueUsername =
jwtHelper.getClaim(configuration.getUniqueUsername(), String.class);
if (StringUtils.isNotBlank(uniqueUsername)) {
LOGGER.info(
"Username remapped from {} to {} based on idToken claims.",
username,
uniqueUsername);
return uniqueUsername;
}
}
}
return username;
}

/**
* Adds authorities to the user based on idToken claims.
*
* @param user the user instance.
* @param idToken the id token.
* @param idToken the idToken containing claims.
*/
protected void addAuthoritiesFromToken(User user, String idToken) {
JWTHelper helper = new JWTHelper(idToken);
Expand All @@ -481,14 +539,56 @@ protected void addAuthoritiesFromToken(User user, String idToken) {
for (String r : roles) {
if (r.equals(Role.ADMIN.name())) user.setRole(Role.ADMIN);
}
for (String g : groups) {

Set<UserGroup> userGroups = user.getGroups();
for (String groupName : groups) {
UserGroup group = null;
if (userGroupService != null) group = userGroupService.get(g);
if (userGroupService != null) {
if (configuration.isGroupNamesUppercase()) {
group = userGroupService.get(groupName.toUpperCase());
}
if (group == null) {
group = userGroupService.get(groupName);
}
}
if (group == null) {
group = new UserGroup();
group.setGroupName(g);
group.setGroupName(
configuration.isGroupNamesUppercase()
? groupName.toUpperCase()
: groupName);
long groupId = -1;
if (userGroupService != null) {
try {
groupId = userGroupService.insert(group);
group = userGroupService.get(groupId);
LOGGER.debug("inserted group id: {}", group.getGroupName());
} catch (BadRequestServiceEx e) {
LOGGER.error("Saving new group found in claims failed");
}
}
}
user.getGroups().add(group);
if (!userGroups.contains(group)) {
try {
if (userGroupService != null)
userGroupService.assignUserGroup(user.getId(), group.getId());
userGroups.add(group);
} catch (NotFoundServiceEx e) {
LOGGER.error(
"Assignment of user {} to group {} failed... skipping it!",
user,
group);
}
}
}

user.setGroups(userGroups);
try {
if (userService != null) userService.update(user);
} catch (BadRequestServiceEx | NotFoundServiceEx e) {
LOGGER.error("Updating user with synchronized groups found in claims failed");
} finally {
LOGGER.info("User updated with the following groups: {}", userGroups);
}
}

Expand Down