From 0279a31d1227e35a26d2e7b95905509c18ec23bc Mon Sep 17 00:00:00 2001 From: mildis Date: Sat, 21 Nov 2020 11:12:26 +0100 Subject: [PATCH] GUACAMOLE-1172: add logic to retrieve groups from OIDC token --- .../openid/AuthenticationProviderService.java | 14 +- .../openid/conf/ConfigurationService.java | 34 +++++ .../openid/token/TokenValidationService.java | 136 +++++++++++++----- .../auth/openid/user/AuthenticatedUser.java | 20 ++- 4 files changed, 162 insertions(+), 42 deletions(-) diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java index 83343269c..30cd1ae8f 100644 --- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java @@ -22,6 +22,7 @@ package org.apache.guacamole.auth.openid; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.Arrays; +import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.auth.openid.conf.ConfigurationService; import org.apache.guacamole.auth.openid.form.TokenField; @@ -34,6 +35,7 @@ import org.apache.guacamole.language.TranslatableMessage; import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.credentials.CredentialsInfo; import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; +import org.jose4j.jwt.JwtClaims; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -91,13 +93,19 @@ public class AuthenticationProviderService { throws GuacamoleException { String username = null; + Set groups = null; // Validate OpenID token in request, if present, and derive username HttpServletRequest request = credentials.getRequest(); if (request != null) { String token = request.getParameter(TokenField.PARAMETER_NAME); - if (token != null) - username = tokenService.processUsername(token); + if (token != null) { + JwtClaims claims = tokenService.validateToken(token); + if (claims != null) { + username = tokenService.processUsername(claims); + groups = tokenService.processGroups(claims); + } + } } // If the username was successfully retrieved from the token, produce @@ -106,7 +114,7 @@ public class AuthenticationProviderService { // Create corresponding authenticated user AuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); - authenticatedUser.init(username, credentials); + authenticatedUser.init(username, credentials, groups); return authenticatedUser; } diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java index 9d889a84f..68c22ef99 100644 --- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java @@ -39,6 +39,12 @@ public class ConfigurationService { */ private static final String DEFAULT_USERNAME_CLAIM_TYPE = "email"; + /** + * The default claim type to use to retrieve an authenticated user's + * groups. + */ + private static final String DEFAULT_GROUPS_CLAIM_TYPE = "groups"; + /** * The default space-separated list of OpenID scopes to request. */ @@ -108,6 +114,18 @@ public class ConfigurationService { }; + /** + * The claim type which contains the authenticated user's groups within + * any valid JWT. + */ + private static final StringGuacamoleProperty OPENID_GROUPS_CLAIM_TYPE = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "openid-groups-claim-type"; } + + }; + /** * The space-separated list of OpenID scopes to request. */ @@ -292,6 +310,22 @@ public class ConfigurationService { return environment.getProperty(OPENID_USERNAME_CLAIM_TYPE, DEFAULT_USERNAME_CLAIM_TYPE); } + /** + * Returns the claim type which contains the authenticated user's groups + * within any valid JWT, as configured with guacamole.properties. By + * default, this will be "groups". + * + * @return + * The claim type which contains the authenticated user's groups + * within any valid JWT, as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public String getGroupsClaimType() throws GuacamoleException { + return environment.getProperty(OPENID_GROUPS_CLAIM_TYPE, DEFAULT_GROUPS_CLAIM_TYPE); + } + /** * Returns the space-separated list of OpenID scopes to request. By default, * this will be "openid email profile". The OpenID scopes determine the diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java index 5efb09dab..72200df3c 100644 --- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java +++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java @@ -20,6 +20,10 @@ package org.apache.guacamole.auth.openid.token; import com.google.inject.Inject; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import org.apache.guacamole.auth.openid.conf.ConfigurationService; import org.apache.guacamole.GuacamoleException; import org.jose4j.jwk.HttpsJwks; @@ -56,23 +60,20 @@ public class TokenValidationService { private NonceService nonceService; /** - * Validates and parses the given ID token, returning the username contained - * therein, as defined by the username claim type given in - * guacamole.properties. If the username claim type is missing or the ID - * token is invalid, null is returned. + * Validates the given ID token, returning the JwtClaims contained therein. + * If the ID token is invalid, null is returned. * * @param token - * The ID token to validate and parse. + * The ID token to validate. * * @return - * The username contained within the given ID token, or null if the ID - * token is not valid or the username claim type is missing, + * The JWT claims contained within the given ID token if it passes tests, + * or null if the token is not valid. * * @throws GuacamoleException * If guacamole.properties could not be parsed. */ - public String processUsername(String token) throws GuacamoleException { - + public JwtClaims validateToken(String token) throws GuacamoleException { // Validating the token requires a JWKS key resolver HttpsJwks jwks = new HttpsJwks(confService.getJWKSEndpoint().toString()); HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(jwks); @@ -89,52 +90,115 @@ public class TokenValidationService { .build(); try { - - String usernameClaim = confService.getUsernameClaimType(); - // Validate JWT JwtClaims claims = jwtConsumer.processToClaims(token); // Verify a nonce is present String nonce = claims.getStringClaimValue("nonce"); - if (nonce == null) { + if (nonce != null) { + // Verify that we actually generated the nonce, and that it has not + // already been used + if (nonceService.isValid(nonce)) { + // nonce is valid, consider claims valid + return claims; + } + else { + logger.info("Rejected OpenID token with invalid/old nonce."); + } + } + else { logger.info("Rejected OpenID token without nonce."); - return null; } + } + // Log any failures to validate/parse the JWT + catch (MalformedClaimException e) { + logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage()); + logger.debug("Malformed claim within received JWT.", e); + } + catch (InvalidJwtException e) { + logger.info("Rejected invalid OpenID token: {}", e.getMessage()); + logger.debug("Invalid JWT received.", e); + } - // Verify that we actually generated the nonce, and that it has not - // already been used - if (!nonceService.isValid(nonce)) { - logger.debug("Rejected OpenID token with invalid/old nonce."); - return null; + return null; + } + + /** + * Parses the given JwtClaims, returning the username contained + * therein, as defined by the username claim type given in + * guacamole.properties. If the username claim type is missing or + * is invalid, null is returned. + * + * @param claims + * A valid JwtClaims to extract the username from. + * + * @return + * The username contained within the given JwtClaims, or null if the + * claim is not valid or the username claim type is missing, + * + * @throws GuacamoleException + * If guacamole.properties could not be parsed. + */ + public String processUsername(JwtClaims claims) throws GuacamoleException { + String usernameClaim = confService.getUsernameClaimType(); + + if (claims != null) { + try { + // Pull username from claims + String username = claims.getStringClaimValue(usernameClaim); + if (username != null) + return username; + } + catch (MalformedClaimException e) { + logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage()); + logger.debug("Malformed claim within received JWT.", e); } - - // Pull username from claims - String username = claims.getStringClaimValue(usernameClaim); - if (username != null) - return username; // Warn if username was not present in token, as it likely means // the system is not set up correctly logger.warn("Username claim \"{}\" missing from token. Perhaps the " + "OpenID scope and/or username claim type are " + "misconfigured?", usernameClaim); - - } - - // Log any failures to validate/parse the JWT - catch (InvalidJwtException e) { - logger.info("Rejected invalid OpenID token: {}", e.getMessage()); - logger.debug("Invalid JWT received.", e); - } - catch (MalformedClaimException e) { - logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage()); - logger.debug("Malformed claim within received JWT.", e); } // Could not retrieve username from JWT return null; - } + /** + * Parses the given JwtClaims, returning the groups contained + * therein, as defined by the groups claim type given in + * guacamole.properties. If the groups claim type is missing or + * is invalid, an empty set is returned. + * + * @param claims + * A valid JwtClaims to extract groups from. + * + * @return + * A Set of String representing the groups the user is member of + * from the OpenID provider point of view, or an empty Set if + * claim is not valid or the groups claim type is missing, + * + * @throws GuacamoleException + * If guacamole.properties could not be parsed. + */ + public Set processGroups(JwtClaims claims) throws GuacamoleException { + String groupsClaim = confService.getGroupsClaimType(); + + if (claims != null) { + try { + // Pull groups from claims + List oidcGroups = claims.getStringListClaimValue(groupsClaim); + if (oidcGroups != null && !oidcGroups.isEmpty()) + return Collections.unmodifiableSet(new HashSet<>(oidcGroups)); + } + catch (MalformedClaimException e) { + logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage()); + logger.debug("Malformed claim within received JWT.", e); + } + } + + // Could not retrieve groups from JWT + return Collections.emptySet(); + } } diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/user/AuthenticatedUser.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/user/AuthenticatedUser.java index b7ff12549..cfc998309 100644 --- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/user/AuthenticatedUser.java +++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/user/AuthenticatedUser.java @@ -20,14 +20,15 @@ package org.apache.guacamole.auth.openid.user; import com.google.inject.Inject; +import java.util.Set; import org.apache.guacamole.net.auth.AbstractAuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; /** * An openid-specific implementation of AuthenticatedUser, associating a - * username and particular set of credentials with the OpenID authentication - * provider. + * username, a particular set of credentials and the groups with the + * OpenID authentication provider. */ public class AuthenticatedUser extends AbstractAuthenticatedUser { @@ -43,6 +44,11 @@ public class AuthenticatedUser extends AbstractAuthenticatedUser { */ private Credentials credentials; + /** + * The groups of the user that was authenticated. + */ + private Set effectiveGroups; + /** * Initializes this AuthenticatedUser using the given username and * credentials. @@ -52,9 +58,13 @@ public class AuthenticatedUser extends AbstractAuthenticatedUser { * * @param credentials * The credentials provided when this user was authenticated. + * + * @param effectiveGroups + * The groups of the user that was authenticated. */ - public void init(String username, Credentials credentials) { + public void init(String username, Credentials credentials, Set effectiveGroups) { this.credentials = credentials; + this.effectiveGroups = effectiveGroups; setIdentifier(username); } @@ -68,4 +78,8 @@ public class AuthenticatedUser extends AbstractAuthenticatedUser { return credentials; } + @Override + public Set getEffectiveUserGroups() { + return effectiveGroups; + } }