diff --git a/extensions/guacamole-auth-saml/pom.xml b/extensions/guacamole-auth-saml/pom.xml index 5029d85f9..135ffacd5 100644 --- a/extensions/guacamole-auth-saml/pom.xml +++ b/extensions/guacamole-auth-saml/pom.xml @@ -158,9 +158,10 @@ - com.sun.jersey - jersey-server - 1.17.1 + javax.ws.rs + jsr311-api + 1.1.1 + provided diff --git a/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/AuthenticationProviderService.java b/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/AuthenticationProviderService.java index db4e08201..e819142d5 100644 --- a/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/AuthenticationProviderService.java @@ -25,8 +25,6 @@ import com.onelogin.saml2.authn.AuthnRequest; import com.onelogin.saml2.authn.SamlResponse; import com.onelogin.saml2.exception.SettingsException; import com.onelogin.saml2.exception.ValidationError; -import com.onelogin.saml2.http.HttpRequest; -import com.onelogin.saml2.servlet.ServletUtils; import com.onelogin.saml2.settings.Saml2Settings; import com.onelogin.saml2.util.Util; import java.io.IOException; @@ -69,6 +67,12 @@ public class AuthenticationProviderService { */ @Inject private Provider authenticatedUserProvider; + + /** + * The map used to track active SAML responses. + */ + @Inject + private SAMLResponseMap samlResponseMap; /** * Returns an AuthenticatedUser representing the user authenticated by the @@ -87,7 +91,7 @@ public class AuthenticationProviderService { */ public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { - + HttpServletRequest request = credentials.getRequest(); // Initialize and configure SAML client. @@ -96,16 +100,18 @@ public class AuthenticationProviderService { if (request != null) { // Look for the SAML Response parameter. - String samlResponseParam = request.getParameter("SAMLResponse"); + String responseHash = Util.urlDecoder(request.getParameter("responseHash")); - if (samlResponseParam != null) { + if (responseHash != null) { - // Convert the SAML response into the version needed for the client. - HttpRequest httpRequest = ServletUtils.makeHttpRequest(request); try { // Generate the response object - SamlResponse samlResponse = new SamlResponse(samlSettings, httpRequest); + if (!samlResponseMap.hasSamlResponse(responseHash)) + throw new GuacamoleInvalidCredentialsException("Provided response has not found.", + CredentialsInfo.USERNAME_PASSWORD); + + SamlResponse samlResponse = samlResponseMap.getSamlResponse(responseHash); if (!samlResponse.validateNumAssertions()) { logger.warn("SAML response contained other than single assertion."); @@ -197,7 +203,6 @@ public class AuthenticationProviderService { })) ); - } } diff --git a/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProvider.java b/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProvider.java index 997922684..a51d1050d 100644 --- a/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProvider.java +++ b/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProvider.java @@ -72,7 +72,8 @@ public class SAMLAuthenticationProvider extends AbstractAuthenticationProvider { throws GuacamoleException { // Attempt to authenticate user with given credentials - AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class); + AuthenticationProviderService authProviderService = + injector.getInstance(AuthenticationProviderService.class); return authProviderService.authenticateUser(credentials); } diff --git a/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderModule.java b/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderModule.java index 9bd5cf20d..faa093550 100644 --- a/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderModule.java @@ -71,9 +71,10 @@ public class SAMLAuthenticationProviderModule extends AbstractModule { bind(AuthenticationProvider.class).toInstance(authProvider); bind(Environment.class).toInstance(environment); - // Bind saml-specific services + // Bind SAML-specific services bind(ConfigurationService.class); bind(SAMLAuthenticationProviderResource.class); + bind(SAMLResponseMap.class); } diff --git a/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderResource.java b/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderResource.java index 338e71491..2dcdffaf0 100644 --- a/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderResource.java +++ b/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLAuthenticationProviderResource.java @@ -20,16 +20,34 @@ package org.apache.guacamole.auth.saml; import com.google.inject.Inject; +import com.onelogin.saml2.authn.SamlResponse; +import com.onelogin.saml2.exception.SettingsException; +import com.onelogin.saml2.exception.ValidationError; +import com.onelogin.saml2.http.HttpRequest; +import com.onelogin.saml2.servlet.ServletUtils; +import com.onelogin.saml2.settings.Saml2Settings; import com.onelogin.saml2.util.Util; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Response; import javax.ws.rs.FormParam; import javax.ws.rs.Path; import javax.ws.rs.POST; +import javax.ws.rs.core.Context; +import javax.xml.bind.DatatypeConverter; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.auth.saml.conf.ConfigurationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; /** * A class that implements the REST API necessary for the @@ -37,19 +55,36 @@ import org.apache.guacamole.auth.saml.conf.ConfigurationService; */ public class SAMLAuthenticationProviderResource { + /** + * Logger for this class. + */ + private final Logger logger = + LoggerFactory.getLogger(SAMLAuthenticationProviderResource.class); + /** * The configuration service for this module. */ @Inject private ConfigurationService confService; + + /** + * The map used to track active responses. + */ + @Inject + private SAMLResponseMap samlResponseMap; /** * A REST endpoint that is POSTed to by the SAML IdP * with the results of the SAML SSO Authentication. * - * @param samlResponse + * @param samlResponseString * The encoded response returned by the SAML IdP. * + * @param consumedRequest + * The HttpServletRequest associated with the SAML response. The + * parameters of this request may not be accessible, as the request may + * have been fully consumed by JAX-RS. + * * @return * A HTTP Response that will redirect the user back to the * Guacamole home page, with the SAMLResponse encoded in the @@ -61,21 +96,61 @@ public class SAMLAuthenticationProviderResource { */ @POST @Path("callback") - public Response processSamlResponse(@FormParam("SAMLResponse") String samlResponse) + public Response processSamlResponse( + @FormParam("SAMLResponse") String samlResponseString, + @Context HttpServletRequest consumedRequest) throws GuacamoleException { - - String guacBase = confService.getCallbackUrl().toString(); - try { - Response redirectHome = Response.seeOther( - new URI(guacBase + "?SAMLResponse=" + Util.urlEncoder(samlResponse))).build(); - return redirectHome; - } - catch (URISyntaxException e) { - throw new GuacamoleServerException("Error processing SAML response.", e); - } - + String guacBase = confService.getCallbackUrl().toString(); + Saml2Settings samlSettings = confService.getSamlSettings(); + try { + HttpRequest request = ServletUtils + .makeHttpRequest(consumedRequest) + .addParameter("SAMLResponse", samlResponseString); + SamlResponse samlResponse = new SamlResponse(samlSettings, request); + + String responseHash = hashSamlResponse(samlResponseString); + samlResponseMap.putSamlResponse(responseHash, samlResponse); + return Response.seeOther(new URI(guacBase + + "?responseHash=" + + Util.urlEncoder(responseHash)) + ).build(); + } + catch (IOException + | NoSuchAlgorithmException + | ParserConfigurationException + | SAXException + | SettingsException + | URISyntaxException + | ValidationError + | XPathExpressionException e) { + throw new GuacamoleServerException(e); + } + + } + + /** + * This is a utility method designed to generate a SHA-256 has for the + * given string representation of the SAMLResponse, throwing an exception + * if, for some reason, the Java implementation in use doesn't support + * SHA-256, and returning a hex-formatted hash value. + * + * @param samlResponse + * The String representation of the SAML response. + * + * @return + * A hex-formatted string of the SHA-256 hash. + * + * @throws NoSuchAlgorithmException + * If Java does not support SHA-256. + */ + private String hashSamlResponse(String samlResponse) + throws NoSuchAlgorithmException { + + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return DatatypeConverter.printHexBinary( + digest.digest(samlResponse.getBytes(StandardCharsets.UTF_8))); } } diff --git a/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLResponseMap.java b/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLResponseMap.java new file mode 100644 index 000000000..3a199045c --- /dev/null +++ b/extensions/guacamole-auth-saml/src/main/java/org/apache/guacamole/auth/saml/SAMLResponseMap.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.saml; + +import com.google.inject.Singleton; +import com.onelogin.saml2.authn.SamlResponse; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A class that handles mapping of hashes to SAMLResponse objects. + */ +@Singleton +public class SAMLResponseMap { + + /** + * The internal data structure that holds a map of SHA-256 hashes to + * SAML responses. + */ + private final ConcurrentMap samlResponseMap = + new ConcurrentHashMap<>(); + + /** + * Retrieve the SamlResponse from the map that is represented by the + * provided hash, or null if no such object exists. + * + * @param hash + * The SHA-256 hash of the SamlResponse. + * + * @return + * The SamlResponse object matching the hash provided. + */ + protected SamlResponse getSamlResponse(String hash) { + return samlResponseMap.remove(hash); + } + + /** + * Place the provided mapping of hash to SamlResponse into the map. + * + * @param hash + * The hash that will be the lookup key for this SamlResponse. + * + * @param samlResponse + * The SamlResponse object. + */ + protected void putSamlResponse(String hash, SamlResponse samlResponse) { + samlResponseMap.put(hash, samlResponse); + } + + /** + * Return true if the provided hash key exists in the map, otherwise false. + * + * @param hash + * The hash key to look for in the map. + * + * @return + * true if the provided hash is present, otherwise false. + */ + protected boolean hasSamlResponse(String hash) { + return samlResponseMap.containsKey(hash); + } + +}