From 8c425535643a0dae5741018a9968b2bceddfcb4a Mon Sep 17 00:00:00 2001 From: mildis Date: Mon, 15 Jan 2024 18:47:52 +0100 Subject: [PATCH] GUACAMOLE-1844 : OIDC JWT claims as user token GUACAMOLE-1844 : OIDC JWT claims as user token This patch allows IDP to send JWT claims that can be mapped to user tokens, prefixed with OIDC_. Same case transormation apply than LDAP_ and CAS_. Define openid-attributes-claim-type with a comma-separated list of claims that should be mapped. Multivalued JWT claims are not unrolled. --- .../openid/AuthenticationProviderService.java | 5 +- .../openid/conf/ConfigurationService.java | 34 +++++++++ .../openid/token/TokenValidationService.java | 70 ++++++++++++++++++- 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java index e83ca092eb..17301f5b64 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java @@ -25,6 +25,7 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; +import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.UriBuilder; @@ -84,6 +85,7 @@ public SSOAuthenticatedUser authenticateUser(Credentials credentials) String username = null; Set groups = null; + Map tokens = Collections.emptyMap(); // Validate OpenID token in request, if present, and derive username HttpServletRequest request = credentials.getRequest(); @@ -94,6 +96,7 @@ public SSOAuthenticatedUser authenticateUser(Credentials credentials) if (claims != null) { username = tokenService.processUsername(claims); groups = tokenService.processGroups(claims); + tokens = tokenService.processAttributes(claims); } } } @@ -104,7 +107,7 @@ public SSOAuthenticatedUser authenticateUser(Credentials credentials) // Create corresponding authenticated user SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); - authenticatedUser.init(username, credentials, groups, Collections.emptyMap()); + authenticatedUser.init(username, credentials, groups, tokens); return authenticatedUser; } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java index 68c22ef990..bbfdea17e8 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java @@ -21,10 +21,13 @@ import com.google.inject.Inject; import java.net.URI; +import java.util.Collections; +import java.util.List; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty; +import org.apache.guacamole.properties.StringListProperty; import org.apache.guacamole.properties.URIGuacamoleProperty; /** @@ -45,6 +48,11 @@ public class ConfigurationService { */ private static final String DEFAULT_GROUPS_CLAIM_TYPE = "groups"; + /** + * The default JWT claims list to map to tokens. + */ + private static final List DEFAULT_ATTRIBUTES_CLAIM_TYPE = Collections.emptyList(); + /** * The default space-separated list of OpenID scopes to request. */ @@ -126,6 +134,16 @@ public class ConfigurationService { }; + /** + * The claims within any valid JWT that should be mapped to + * the authenticated user's tokens, as configured with guacamole.properties. + */ + private static final StringListProperty OPENID_ATTRIBUTES_CLAIM_TYPE = + new StringListProperty() { + @Override + public String getName() { return "openid-attributes-claim-type"; } + }; + /** * The space-separated list of OpenID scopes to request. */ @@ -326,6 +344,22 @@ public String getGroupsClaimType() throws GuacamoleException { return environment.getProperty(OPENID_GROUPS_CLAIM_TYPE, DEFAULT_GROUPS_CLAIM_TYPE); } + /** + * Returns the claims list within any valid JWT that should be mapped to + * the authenticated user's tokens, as configured with guacamole.properties. + * Empty by default. + * + * @return + * The claims list within any valid JWT that should be mapped to + * the authenticated user's tokens, as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public List getAttributesClaimType() throws GuacamoleException { + return environment.getProperty(OPENID_ATTRIBUTES_CLAIM_TYPE, DEFAULT_ATTRIBUTES_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-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java index b9c2add2ae..2fad48ce4a 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java @@ -21,12 +21,15 @@ import com.google.inject.Inject; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; -import org.apache.guacamole.auth.openid.conf.ConfigurationService; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.openid.conf.ConfigurationService; import org.apache.guacamole.auth.sso.NonceService; +import org.apache.guacamole.token.TokenName; import org.jose4j.jwk.HttpsJwks; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.MalformedClaimException; @@ -48,6 +51,11 @@ public class TokenValidationService { */ private final Logger logger = LoggerFactory.getLogger(TokenValidationService.class); + /** + * The prefix to use when generating token names. + */ + public static final String OIDC_ATTRIBUTE_TOKEN_PREFIX = "OIDC_"; + /** * Service for retrieving OpenID configuration information. */ @@ -202,4 +210,64 @@ public Set processGroups(JwtClaims claims) throws GuacamoleException { // Could not retrieve groups from JWT return Collections.emptySet(); } + + /** + * Parses the given JwtClaims, returning the attributes contained + * therein, as defined by the attributes claim type given in + * guacamole.properties. If the attributes claim type is missing or + * is invalid, an empty set is returned. + * + * @param claims + * A valid JwtClaims to extract attributes from. + * + * @return + * A Map of String,String representing the attributes and values + * from the OpenID provider point of view, or an empty Map if + * claim is not valid or the attributes claim type is missing. + * + * @throws GuacamoleException + * If guacamole.properties could not be parsed. + */ + public Map processAttributes(JwtClaims claims) throws GuacamoleException { + List attributesClaim = confService.getAttributesClaimType(); + + if (claims != null && !attributesClaim.isEmpty()) { + try { + logger.debug("Iterating over attributes claim list : {}", attributesClaim); + + // We suppose all claims are resolved, so the hashmap is initialised to + // the size of the configuration list + Map tokens = new HashMap(attributesClaim.size()); + + // We iterate over the configured attributes + for (String key: attributesClaim) { + // Retrieve the corresponding claim + String oidcAttr = claims.getStringClaimValue(key); + + // We do have a matching claim and it is not empty + if (oidcAttr != null && !oidcAttr.isEmpty()) { + // append the prefixed claim value to the token map with its value + String tokenName = TokenName.canonicalize(key, OIDC_ATTRIBUTE_TOKEN_PREFIX); + tokens.put(tokenName, oidcAttr); + logger.debug("Claim {} found and set to {}", key, tokenName); + } + else { + // wanted attribute is not found in the claim + logger.debug("Claim {} not found in JWT.", key); + } + } + + // We did process all the expected claims + return Collections.unmodifiableMap(tokens); + } + catch (MalformedClaimException e) { + logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage()); + logger.debug("Malformed claim within received JWT.", e); + } + } + + // Could not retrieve attributes from JWT + logger.debug("Attributes claim not defined. Returning empty map."); + return Collections.emptyMap(); + } }